Compare commits
130 changed files with 3936 additions and 6118 deletions
13
.env.example
13
.env.example
|
|
@ -11,11 +11,6 @@ MCP_REGISTRY_DATABASE_URL=postgres://username:password@localhost:5432/mcp-regist
|
||||||
# For offline development, use: data/seed.json
|
# For offline development, use: data/seed.json
|
||||||
MCP_REGISTRY_SEED_FROM=https://registry.modelcontextprotocol.io/v0/servers
|
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
|
# GitHub OAuth configuration
|
||||||
# These creds are for local development with the 'MCP Registry Login (Local)' GitHub App
|
# 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
|
# They don't provide any real privileged access, hence why it's okay that they're here
|
||||||
|
|
@ -32,14 +27,6 @@ MCP_REGISTRY_JWT_PRIVATE_KEY=bb2c6b424005acd5df47a9e2c87f446def86dd740c888ea3efb
|
||||||
# This should be disabled in prod
|
# This should be disabled in prod
|
||||||
MCP_REGISTRY_ENABLE_ANONYMOUS_AUTH=false
|
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
|
# Google Cloud Identity OIDC configuration for admin access
|
||||||
# Enable OIDC authentication for @modelcontextprotocol.io admin accounts
|
# Enable OIDC authentication for @modelcontextprotocol.io admin accounts
|
||||||
MCP_REGISTRY_OIDC_ENABLED=false
|
MCP_REGISTRY_OIDC_ENABLED=false
|
||||||
|
|
|
||||||
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
|
## Development
|
||||||
|
|
||||||
- Use `make` targets where possible (run `make help` to see available targets)
|
- 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
|
- 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
|
- package-ecosystem: gomod
|
||||||
directory: /
|
directory: /
|
||||||
groups:
|
groups:
|
||||||
go-dependencies:
|
opentelemetry:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "go.opentelemetry.io/*"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
- package-ecosystem: gomod
|
- package-ecosystem: gomod
|
||||||
directory: /deploy
|
directory: /deploy
|
||||||
groups:
|
groups:
|
||||||
deploy-go-dependencies:
|
opentelemetry:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "go.opentelemetry.io/*"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
- package-ecosystem: github-actions
|
- package-ecosystem: github-actions
|
||||||
directory: /
|
directory: /
|
||||||
groups:
|
groups:
|
||||||
github-actions:
|
actions:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
- package-ecosystem: docker
|
- package-ecosystem: "docker"
|
||||||
directory: /
|
directory: "/"
|
||||||
schedule:
|
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 }}
|
environment: ${{ inputs.environment }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Setup Pulumi
|
- name: Setup Pulumi
|
||||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
uses: pulumi/actions@8582a9e8cc630786854029b4e09281acd6794b58
|
||||||
with:
|
with:
|
||||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a
|
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20
|
||||||
with:
|
with:
|
||||||
version: v2.13.1
|
version: v2.11.4
|
||||||
|
|
||||||
- name: Validate schemas and examples
|
- name: Validate schemas and examples
|
||||||
run: make validate
|
run: make validate
|
||||||
|
|
@ -48,16 +48,16 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up ko
|
- name: Set up ko
|
||||||
uses: ko-build/setup-ko@v0.10
|
uses: ko-build/setup-ko@v0.9
|
||||||
|
|
||||||
- name: Run all tests
|
- name: Run all tests
|
||||||
run: make test-all
|
run: make test-all
|
||||||
|
|
|
||||||
2
.github/workflows/claude.yml
vendored
2
.github/workflows/claude.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
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
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Setup Pulumi
|
- name: Setup Pulumi
|
||||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
uses: pulumi/actions@8582a9e8cc630786854029b4e09281acd6794b58
|
||||||
with:
|
with:
|
||||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
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
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up ko
|
- name: Set up ko
|
||||||
uses: ko-build/setup-ko@v0.10
|
uses: ko-build/setup-ko@v0.9
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|
@ -70,16 +70,16 @@ jobs:
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Setup Pulumi
|
- name: Setup Pulumi
|
||||||
uses: pulumi/actions@8e5e406f4007fca908480587cb9893c07090f58d
|
uses: pulumi/actions@8582a9e8cc630786854029b4e09281acd6794b58
|
||||||
with:
|
with:
|
||||||
pulumi-version: ${{ env.PULUMI_VERSION }}
|
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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
|
||||||
- name: Install Syft
|
- name: Install Syft
|
||||||
uses: anchore/sbom-action/download-syft@v0.24.2
|
uses: anchore/sbom-action/download-syft@v0.24.0
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94
|
uses: goreleaser/goreleaser-action@e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: v2.12.0
|
version: v2.12.0
|
||||||
|
|
@ -45,19 +45,19 @@ jobs:
|
||||||
needs: goreleaser
|
needs: goreleaser
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c
|
||||||
with:
|
with:
|
||||||
go-version: 'stable'
|
go-version: 'stable'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Set up ko
|
- name: Set up ko
|
||||||
uses: ko-build/setup-ko@v0.10
|
uses: ko-build/setup-ko@v0.9
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|
|
||||||
4
.github/workflows/sync-schema.yml
vendored
4
.github/workflows/sync-schema.yml
vendored
|
|
@ -15,10 +15,10 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Checkout static repo
|
- name: Checkout static repo
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
repository: modelcontextprotocol/static
|
repository: modelcontextprotocol/static
|
||||||
path: static-repo
|
path: static-repo
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ linters:
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- godox
|
- godox
|
||||||
- gomoddirectives
|
- gomoddirectives
|
||||||
- gomodguard_v2
|
- gomodguard
|
||||||
- goprintffuncname
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- grouper
|
- grouper
|
||||||
|
|
@ -55,10 +55,6 @@ linters:
|
||||||
max-complexity: 20
|
max-complexity: 20
|
||||||
dupl:
|
dupl:
|
||||||
threshold: 300
|
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:
|
gocognit:
|
||||||
min-complexity: 51
|
min-complexity: 51
|
||||||
nestif:
|
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
|
### Running checks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run lint, schema validation, and all tests
|
# Run lint, unit tests and integration tests
|
||||||
# Note: this also runs `make dev-down`, stopping any running dev-compose environment
|
|
||||||
make check
|
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)).
|
**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:
|
Current key maintainers:
|
||||||
- **Radoslav (Rado) Dimitrov** (Stacklok) [@rdimitrov](https://github.com/rdimitrov) - WG Lead
|
- **Adam Jones** (Anthropic) [@domdomegg](https://github.com/domdomegg)
|
||||||
- **Tadas Antanavicius** (PulseMCP) [@tadasant](https://github.com/tadasant)
|
- **Tadas Antanavicius** (PulseMCP) [@tadasant](https://github.com/tadasant)
|
||||||
- **Bob Dickinson** (TeamSpark) [@BobDickinson](https://github.com/BobDickinson)
|
- **Toby Padilla** (GitHub) [@toby](https://github.com/toby)
|
||||||
- **Preeti (Pree) Dewani** (Ravenmail) [@pree-dew](https://github.com/pree-dew)
|
- **Radoslav (Rado) Dimitrov** (Stacklok) [@rdimitrov](https://github.com/rdimitrov)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -32,9 +32,9 @@ Often (but not always) ideas flow through this pipeline:
|
||||||
#### Pre-requisites
|
#### Pre-requisites
|
||||||
|
|
||||||
- **Docker**
|
- **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/))
|
- **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
|
#### Running the server
|
||||||
|
|
||||||
|
|
@ -94,8 +94,7 @@ See [the publisher guide](./docs/modelcontextprotocol-io/quickstart.mdx) for mor
|
||||||
#### Other commands
|
#### Other commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run lint, schema validation, and all tests
|
# Run lint, unit tests and integration tests
|
||||||
# Note: this also runs `make dev-down`, stopping any running dev-compose environment
|
|
||||||
make check
|
make check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -111,8 +110,7 @@ For Claude and other AI tools: Always prefer make targets over custom commands w
|
||||||
|
|
||||||
```
|
```
|
||||||
├── cmd/ # Application entry points
|
├── cmd/ # Application entry points
|
||||||
│ ├── publisher/ # Server publishing tool
|
│ └── publisher/ # Server publishing tool
|
||||||
│ └── registry/ # Registry API server
|
|
||||||
├── data/ # Seed data
|
├── data/ # Seed data
|
||||||
├── deploy/ # Deployment configuration (Pulumi)
|
├── deploy/ # Deployment configuration (Pulumi)
|
||||||
├── docs/ # Documentation
|
├── 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)
|
│ ├── auth/ # Authentication (GitHub OAuth, JWT, namespace blocking)
|
||||||
│ ├── config/ # Configuration management
|
│ ├── config/ # Configuration management
|
||||||
│ ├── database/ # Data persistence (PostgreSQL)
|
│ ├── database/ # Data persistence (PostgreSQL)
|
||||||
│ ├── importer/ # Seed data import
|
|
||||||
│ ├── service/ # Business logic
|
│ ├── service/ # Business logic
|
||||||
│ ├── telemetry/ # Metrics and monitoring
|
│ ├── telemetry/ # Metrics and monitoring
|
||||||
│ └── validators/ # Input validation
|
│ └── validators/ # Input validation
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,9 @@ make publisher
|
||||||
make dev-compose # Start local registry
|
make dev-compose # Start local registry
|
||||||
./bin/mcp-publisher init
|
./bin/mcp-publisher init
|
||||||
./bin/mcp-publisher login none --registry=http://localhost:8080
|
./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
|
## Architecture
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
@ -27,7 +24,6 @@ flag — passing one would be read as the `server.json` path.
|
||||||
- **`login`** - Handle authentication (github, dns, http, none)
|
- **`login`** - Handle authentication (github, dns, http, none)
|
||||||
- **`publish`** - Validate and upload servers to registry
|
- **`publish`** - Validate and upload servers to registry
|
||||||
- **`status`** - Update server lifecycle status (active, deprecated, deleted)
|
- **`status`** - Update server lifecycle status (active, deprecated, deleted)
|
||||||
- **`validate`** - Validate server.json without publishing
|
|
||||||
- **`logout`** - Clear stored credentials
|
- **`logout`** - Clear stored credentials
|
||||||
|
|
||||||
### Authentication Providers
|
### 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
|
- **`main.go`** - CLI setup and command routing
|
||||||
- **`commands/`** - Command implementations with auto-detection logic
|
- **`commands/`** - Command implementations with auto-detection logic
|
||||||
- **`auth/`** - Authentication provider implementations
|
- **`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.")
|
fmt.Fprintln(os.Stdout, "Successfully read the public key from Key Vault.")
|
||||||
auth.PrintEcdsaP384KeyInfo(ecdsa.PublicKey{
|
auth.PrintEcdsaP384KeyInfo(ecdsa.PublicKey{
|
||||||
Curve: elliptic.P384(),
|
Curve: elliptic.P384(),
|
||||||
X: new(big.Int).SetBytes(keyResp.Key.X), //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
X: new(big.Int).SetBytes(keyResp.Key.X),
|
||||||
Y: new(big.Int).SetBytes(keyResp.Key.Y), //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
Y: new(big.Int).SetBytes(keyResp.Key.Y),
|
||||||
})
|
})
|
||||||
|
|
||||||
timestamp := auth.GetTimestamp()
|
timestamp := auth.GetTimestamp()
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,10 @@ func parseRawPrivateKey(curve elliptic.Curve, privateKeyBytes []byte) (*ecdsa.Pr
|
||||||
return &ecdsa.PrivateKey{
|
return &ecdsa.PrivateKey{
|
||||||
PublicKey: ecdsa.PublicKey{
|
PublicKey: ecdsa.PublicKey{
|
||||||
Curve: curve,
|
Curve: curve,
|
||||||
X: x, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
X: x,
|
||||||
Y: y, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
Y: y,
|
||||||
},
|
},
|
||||||
D: d, //nolint:staticcheck // SA1019: needs crypto/ecdh refactor
|
D: d,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,31 +17,6 @@ const (
|
||||||
GitHubAccessTokenURL = "https://github.com/login/oauth/access_token" // #nosec:G101
|
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
|
// DeviceCodeResponse represents the response from GitHub's device code endpoint
|
||||||
type DeviceCodeResponse struct {
|
type DeviceCodeResponse struct {
|
||||||
DeviceCode string `json:"device_code"`
|
DeviceCode string `json:"device_code"`
|
||||||
|
|
@ -57,38 +32,6 @@ type AccessTokenResponse struct {
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope"`
|
||||||
Error string `json:"error,omitempty"`
|
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
|
// RegistryTokenResponse represents the response from registry's token exchange endpoint
|
||||||
|
|
@ -103,24 +46,6 @@ type GitHubATProvider struct {
|
||||||
registryURL string
|
registryURL string
|
||||||
providedToken string // Token provided via --token flag or MCP_GITHUB_TOKEN env var
|
providedToken string // Token provided via --token flag or MCP_GITHUB_TOKEN env var
|
||||||
githubToken string // In-memory GitHub token set by Login()
|
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
|
// ServerHealthResponse represents the response from the health endpoint
|
||||||
|
|
@ -137,13 +62,8 @@ func NewGitHubATProvider(registryURL, token string) Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &GitHubATProvider{
|
return &GitHubATProvider{
|
||||||
registryURL: registryURL,
|
registryURL: registryURL,
|
||||||
providedToken: token,
|
providedToken: token,
|
||||||
accessTokenURL: GitHubAccessTokenURL,
|
|
||||||
deviceCodeURL: GitHubDeviceCodeURL,
|
|
||||||
pollInterval: defaultPollInterval,
|
|
||||||
expiresIn: defaultExpiresIn,
|
|
||||||
sleep: time.Sleep,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -229,7 +149,7 @@ func (g *GitHubATProvider) requestDeviceCode(ctx context.Context) (string, strin
|
||||||
return "", "", "", err
|
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 {
|
if err != nil {
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
}
|
}
|
||||||
|
|
@ -258,21 +178,6 @@ func (g *GitHubATProvider) requestDeviceCode(ctx context.Context) (string, strin
|
||||||
return "", "", "", err
|
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
|
return deviceCodeResp.DeviceCode, deviceCodeResp.UserCode, deviceCodeResp.VerificationURI, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,28 +198,13 @@ func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pacing comes from the device-code response (via requestDeviceCode),
|
// Default polling interval and expiration time
|
||||||
// falling back to GitHub's documented defaults.
|
interval := 5 // seconds
|
||||||
interval := g.pollInterval
|
expiresIn := 900 // 15 minutes
|
||||||
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
|
|
||||||
}
|
|
||||||
deadline := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
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) {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -333,54 +223,20 @@ func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// A transient 5xx must not abandon a login the user may already have
|
var tokenResp AccessTokenResponse
|
||||||
// approved in the browser. Each consecutive 5xx grows the wait by 5s up
|
err = json.Unmarshal(body, &tokenResp)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per RFC 8628 §3.5, both authorization_pending and slow_down indicate
|
if tokenResp.Error == "authorization_pending" {
|
||||||
// the client should keep polling. slow_down additionally requires that
|
// User hasn't authorized yet, wait and retry
|
||||||
// the polling interval be increased by 5 seconds.
|
time.Sleep(time.Duration(interval) * time.Second)
|
||||||
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)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenResp.Error != "" {
|
if tokenResp.Error != "" {
|
||||||
failure := fmt.Errorf("token request failed: %s", tokenResp.errorDetail())
|
return "", fmt.Errorf("token request failed: %s", tokenResp.Error)
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenResp.AccessToken != "" {
|
if tokenResp.AccessToken != "" {
|
||||||
|
|
@ -391,9 +247,6 @@ func (g *GitHubATProvider) pollForToken(ctx context.Context, deviceCode string)
|
||||||
return "", fmt.Errorf("failed to obtain access token")
|
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")
|
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