fix(admin): use PATCH status endpoints in takedown script and runbook (#1518)

The admin takedown script cannot work as written, and the runbook that
documents it has the same problem.

## The bug

`tools/admin/takedown.sh` sent `PUT` with a `?status=deleted` query
parameter to two routes:

| What the script sent | Reality |
|---|---|
| `PUT /v0/servers/{name}/versions/{version}?status=deleted` | Route
exists, but requires a full `ServerJSON` body and has no `status` query
parameter |
| `PUT /v0/servers/{name}?status=deleted` | **Not registered at all** —
the whole-server takedown path 404s |

The real endpoints are `PATCH
/v0/servers/{name}/versions/{version}/status` and `PATCH
/v0/servers/{name}/status`
([status.go:83](https://github.com/modelcontextprotocol/registry/blob/main/internal/api/handlers/v0/status.go#L83),
[:198](https://github.com/modelcontextprotocol/registry/blob/main/internal/api/handlers/v0/status.go#L198)),
both taking `{"status": ..., "statusMessage": ...}`. `edit.go` even says
so in its own description: use the status endpoint for status changes.

## Script changes

- Send `PATCH` with a JSON body built via `jq`
- **Require `VERSION` or `ALL_VERSIONS=true` explicitly.** Previously a
missing `VERSION` meant "entire server". Since `PATCH .../status`
updates *every* version, an operator who simply forgot `VERSION` would
have taken down the whole server. Now that needs saying out loud.
- Support `STATUS` (default `deleted`) and `STATUS_MESSAGE`, so a
takedown can record its reason
- `set -euo pipefail`, non-zero exit on HTTP errors, and print the
response body so huma error detail is visible instead of being swallowed

## Runbook changes

- `GET /v0/servers/{name}` is not a route — use `GET
.../versions/latest` (which is supported, see
[servers.go:155-178](https://github.com/modelcontextprotocol/registry/blob/main/internal/api/handlers/v0/servers.go#L155))
- Replace the per-version `?status=deleted` loop with the single
all-versions `PATCH .../status` call, which reports `updatedCount`
- Keep the per-version `PUT` loop for *content* edits, which genuinely
has no bulk endpoint, and show that it needs the full body
- Correct the closing note claiming server-wide changes must be applied
per version — true for content, not for status
- Fix the broken `moderation-guidelines.md` link (dead since the docs
restructuring; the file is now
`modelcontextprotocol-io/moderation-policy.mdx`)

## Verification

`shellcheck` clean, `bash -n` clean, and all three guard paths exercised
(no args / neither VERSION nor ALL_VERSIONS / both set). Both `PATCH`
paths confirmed present in `docs/reference/api/openapi.yaml`. I did not
run a live takedown against production.

Kept on `/v0` rather than `/v0.1` to match the existing admin/deploy
convention — happy to switch if you would rather admin tooling target
the frozen version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Radoslav Dimitrov 2026-08-10 16:24:06 +03:00 • committed by GitHub
commit 43fbc1dcbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 135 additions and 43 deletions

View file

@ -1,6 +1,6 @@
# Admin Operations
This is a brief guide for admins and moderators managing content on the registry. All actions should be taken in line with the [moderation guidelines](moderation-guidelines.md).
This is a brief guide for admins and moderators managing content on the registry. All actions should be taken in line with the [moderation policy](../modelcontextprotocol-io/moderation-policy.mdx).
## Prerequisites
@ -33,8 +33,8 @@ ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g')
# Get specific version
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}" > server.json
# Or get latest version (omit /versions/VERSION)
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}" > server.json
# Or get the latest version (use the special version "latest")
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/latest" > server.json
```
### Step 2: Make Changes
@ -44,19 +44,46 @@ Open `server.json` and edit the specific version details. You cannot change the
### Step 3: Update Version
```bash
# Update specific version
# Update specific version (requires the full server.json body)
curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}" \
-H "Authorization: Bearer ${REGISTRY_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(cat server.json)"
```
To change **only** the status of a version, use the status endpoint instead — it does not require
the full server configuration:
```bash
curl -X PATCH "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}/status" \
-H "Authorization: Bearer ${REGISTRY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status": "deprecated", "statusMessage": "Superseded by v2"}'
```
## Edit an Entire Server (All Versions)
Use this when you need to apply changes across all versions of a server (e.g., mark entire server as deleted, apply content scrubbing).
### Status Changes Across All Versions
### Step 1: List All Versions
A status change applies to every version of a server in a single request. The response reports
how many versions were updated in `updatedCount`.
```bash
export SERVER_NAME="<server-name>" # e.g., "com.example/my-server"
ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g')
curl -X PATCH "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/status" \
-H "Authorization: Bearer ${REGISTRY_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status": "deleted", "statusMessage": "Removed per moderation policy"}'
```
### Content Changes Across All Versions
Content edits (e.g. scrubbing sensitive text from descriptions) have no bulk endpoint and must be
applied per version using the edit endpoint, which takes the full server configuration.
#### Step 1: List All Versions
```bash
export SERVER_NAME="<server-name>" # e.g., "com.example/my-server"
@ -65,29 +92,32 @@ ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g')
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions" > all_versions.json
```
### Step 2: Extract Versions
#### Step 2: Extract Versions
```bash
# Extract all versions from the server
jq -r '.servers[].server.version' all_versions.json > versions.txt
```
### Step 3: Apply Changes to All Versions
#### Step 3: Apply Changes to Each Version
```bash
# For each version, apply changes using the edit endpoint
while read VERSION; do
echo "Processing version: $VERSION"
# Apply your changes here (e.g., set status to "deleted")
curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}?status=deleted" \
# Download the version, edit it, then send the full body back
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}" > version.json
# Apply your changes to version.json here, then:
curl -X PUT "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}" \
-H "Authorization: Bearer ${REGISTRY_TOKEN}" \
-H "Content-Type: application/json"
-H "Content-Type: application/json" \
-d "$(cat version.json)"
done < versions.txt
# Clean up temporary files
rm versions.txt all_versions.json
rm -f versions.txt all_versions.json version.json
```
## Quick Operations
@ -98,7 +128,7 @@ rm versions.txt all_versions.json
export SERVER_NAME="<server-name>" # e.g., "com.example/my-server"
ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g')
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}" > latest_version.json
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/latest" > latest_version.json
export VERSION=$(jq -r '.server.version' latest_version.json)
echo "Latest version: $VERSION"
```
@ -113,30 +143,37 @@ export REGISTRY_TOKEN="<your-token>"
REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" VERSION="$VERSION" ./tools/admin/takedown.sh
```
### Takedown Latest Version (Entire Server)
### Takedown All Versions of a Server
`ALL_VERSIONS=true` marks every version as deleted in a single request. The script requires either
`VERSION` or `ALL_VERSIONS` to be set explicitly, so a forgotten `VERSION` cannot take down a whole
server by accident.
```bash
export SERVER_NAME="<server-name>" # e.g., "com.example/my-server"
export REGISTRY_TOKEN="<your-token>"
# This marks the latest version as deleted
REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" ./tools/admin/takedown.sh
REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" ALL_VERSIONS=true ./tools/admin/takedown.sh
```
### Takedown All Versions of a Server
### Takedown the Latest Version Only
```bash
export SERVER_NAME="<server-name>" # e.g., "com.example/my-server"
export REGISTRY_TOKEN="<your-token>"
ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g')
# Get all versions and takedown each one
curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions" | \
jq -r '.servers[].server.version' | \
while read VERSION; do
echo "Taking down version: $VERSION"
REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" VERSION="$VERSION" ./tools/admin/takedown.sh
done
# Resolve the latest version, then take down that specific version
VERSION=$(curl -s "https://registry.modelcontextprotocol.io/v0/servers/${ENCODED_SERVER_NAME}/versions/latest" | jq -r '.server.version')
REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" VERSION="$VERSION" ./tools/admin/takedown.sh
```
### Record a Reason with a Takedown
```bash
REGISTRY_TOKEN="$REGISTRY_TOKEN" SERVER_NAME="$SERVER_NAME" ALL_VERSIONS=true \
STATUS_MESSAGE="Removed per moderation policy" ./tools/admin/takedown.sh
```
## Connecting to the Production Database
@ -179,6 +216,8 @@ Any write attempts will fail with an error until you disconnect.
## Notes
- **Version-specific changes**: Only affect that particular version
- **Server-wide changes**: Must be applied to each version individually
- **Server-wide status changes**: `PATCH /v0/servers/{serverName}/status` updates every version in one request
- **Server-wide content changes**: Have no bulk endpoint and must be applied to each version individually
- **Status vs. edit**: Use `PATCH .../status` to change status alone; the `PUT` edit endpoint requires the full server configuration
- **Content scrubbing**: Use the version-specific edit workflow to scrub sensitive content
- **Server name**: Cannot be changed in any version (it's the immutable identifier)

View file

@ -1,30 +1,83 @@
#!/bin/bash
# Simple takedown script
# Set the lifecycle status of an MCP server, either for one version or for every version.
set -euo pipefail
REGISTRY_URL="${REGISTRY_URL:-https://registry.modelcontextprotocol.io}"
STATUS="${STATUS:-deleted}"
SERVER_NAME="${SERVER_NAME:-}"
VERSION="${VERSION:-}"
ALL_VERSIONS="${ALL_VERSIONS:-}"
STATUS_MESSAGE="${STATUS_MESSAGE:-}"
if [ -z "$SERVER_NAME" ] || [ -z "$REGISTRY_TOKEN" ]; then
echo "Usage: REGISTRY_TOKEN=<token> SERVER_NAME=<server-name> [VERSION=<version>] $0"
echo "Example: REGISTRY_TOKEN=token SERVER_NAME=com.example/my-server ./takedown.sh"
echo "Example: REGISTRY_TOKEN=token SERVER_NAME=com.example/my-server VERSION=1.0.0 ./takedown.sh"
usage() {
cat <<'EOF'
Usage:
REGISTRY_TOKEN=<token> SERVER_NAME=<server-name> VERSION=<version> ./takedown.sh
REGISTRY_TOKEN=<token> SERVER_NAME=<server-name> ALL_VERSIONS=true ./takedown.sh
Examples:
REGISTRY_TOKEN=token SERVER_NAME=com.example/my-server VERSION=1.0.0 ./takedown.sh
REGISTRY_TOKEN=token SERVER_NAME=com.example/my-server ALL_VERSIONS=true ./takedown.sh
Optional environment variables:
STATUS active | deprecated | deleted (default: deleted)
STATUS_MESSAGE reason recorded alongside the status change (max 500 characters)
REGISTRY_URL registry base URL (default: https://registry.modelcontextprotocol.io)
EOF
}
if [ -z "$SERVER_NAME" ] || [ -z "${REGISTRY_TOKEN:-}" ]; then
usage
exit 1
fi
# URL encode the server name (replace / with %2F)
ENCODED_SERVER_NAME=$(echo "$SERVER_NAME" | sed 's|/|%2F|g')
# Determine the endpoint based on whether VERSION is provided
if [ -n "$VERSION" ]; then
# Mark specific version as deleted
ENDPOINT="${REGISTRY_URL}/v0/servers/${ENCODED_SERVER_NAME}/versions/${VERSION}?status=deleted"
echo "Marking version $VERSION of server $SERVER_NAME as deleted..."
else
# Mark entire server as deleted (latest version)
ENDPOINT="${REGISTRY_URL}/v0/servers/${ENCODED_SERVER_NAME}?status=deleted"
echo "Marking server $SERVER_NAME as deleted..."
# Require an explicit choice, so that a forgotten VERSION cannot silently take
# down every version of a server.
if [ -n "$VERSION" ] && [ -n "$ALL_VERSIONS" ]; then
echo "Error: set either VERSION or ALL_VERSIONS, not both." >&2
exit 1
fi
if [ -z "$VERSION" ] && [ -z "$ALL_VERSIONS" ]; then
echo "Error: set VERSION=<version> for a single version, or ALL_VERSIONS=true for every version." >&2
echo >&2
usage
exit 1
fi
# Update server status to deleted
curl -X PUT "$ENDPOINT" \
# URL encode the server name and version (replace / with %2F)
ENCODED_SERVER_NAME="${SERVER_NAME//\//%2F}"
if [ -n "$VERSION" ]; then
ENCODED_VERSION="${VERSION//\//%2F}"
ENDPOINT="${REGISTRY_URL}/v0/servers/${ENCODED_SERVER_NAME}/versions/${ENCODED_VERSION}/status"
echo "Setting version ${VERSION} of ${SERVER_NAME} to '${STATUS}'..."
else
ENDPOINT="${REGISTRY_URL}/v0/servers/${ENCODED_SERVER_NAME}/status"
echo "Setting ALL versions of ${SERVER_NAME} to '${STATUS}'..."
fi
if [ -n "$STATUS_MESSAGE" ]; then
BODY=$(jq -n --arg status "$STATUS" --arg message "$STATUS_MESSAGE" \
'{status: $status, statusMessage: $message}')
else
BODY=$(jq -n --arg status "$STATUS" '{status: $status}')
fi
RESPONSE_BODY=$(mktemp)
trap 'rm -f "$RESPONSE_BODY"' EXIT
HTTP_CODE=$(curl -sS -X PATCH "$ENDPOINT" \
-H "Authorization: Bearer ${REGISTRY_TOKEN}" \
-H "Content-Type: application/json"
-H "Content-Type: application/json" \
-d "$BODY" \
-o "$RESPONSE_BODY" \
-w '%{http_code}')
cat "$RESPONSE_BODY"
echo
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
echo "Request failed with HTTP ${HTTP_CODE}" >&2
exit 1
fi