registry/tools/admin/takedown.sh
Radoslav Dimitrov 43fbc1dcbd
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>
2026-08-10 16:24:06 +03:00

83 lines
2.6 KiB
Shell
Executable file

#!/bin/bash
# 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:-}"
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
# 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
# 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" \
-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