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>
83 lines
2.6 KiB
Shell
Executable file
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
|