Practical guide

MCP Server Registry API: How to List New and Updated MCP Servers (2026)

Last reviewed: October 27, 2026

List published MCP servers, export the latest configurations, and compare snapshots for new names, versions and remote endpoints. Start with curl and a Python script, then schedule a watch when repeat collection becomes useful.

What the official MCP Registry is

The official registry at registry.modelcontextprotocol.io exposes published Model Context Protocol server metadata through a public JSON API. A publisher's server.json describes a server's identity, version and connection or package configuration. The API wraps that configuration in a server object and adds registry-managed _meta metadata.

A namespaced name such as io.github.user/weather has a reverse-DNS namespace and a server name separated by a slash. The full name identifies the server; version identifies a published version of its configuration. A listing without a version filter can contain several records with the same name. A server version and a package's own version are separate fields.

remotes describes published remote connection configurations, including a transport type and endpoint url. packages describes installable distributions, including registryType, identifier and transport. A server can publish remotes, packages or both. Examples of transport types include streamable-http, sse and stdio; package registry types can include npm, PyPI, OCI, NuGet and MCPB.

A registry listing is not a security audit or endorsement. It does not prove that a package is safe, a remote is available, or a server is appropriate for your agent's permissions. Treat the metadata as discovery evidence and review the code, destination and requested access before approving use.

Query the registry API with curl

GET /v0/servers returns servers, an array of objects containing server and _meta, plus a metadata object. The page limit is 1 to 100, with a default of 30. Read metadata.nextCursor and repeat until it is absent or empty. metadata.count describes the returned page, not the total registry size.

# List latest versions. Public reads do not need an API key.
curl -f -sS --connect-timeout 5 --max-time 25 --get \
  'https://registry.modelcontextprotocol.io/v0/servers' \
  --data-urlencode 'limit=100' --data-urlencode 'version=latest' \
  --data-urlencode 'include_deleted=true' -o page1.json

# Follow the exact cursor, keeping the same filters.
CURSOR=$(python3 -c 'import json; print(json.load(open("page1.json"))["metadata"].get("nextCursor") or "")')
if [ -n "$CURSOR" ]; then
  sleep 1
  curl -f -sS --connect-timeout 5 --max-time 25 --get \
    'https://registry.modelcontextprotocol.io/v0/servers' \
    --data-urlencode 'limit=100' --data-urlencode 'version=latest' \
    --data-urlencode 'include_deleted=true' --data-urlencode "cursor=$CURSOR" -o page2.json
fi

# Name substring search, not full-text search of descriptions.
curl -f -sS --connect-timeout 5 --max-time 25 --get \
  'https://registry.modelcontextprotocol.io/v0/servers' \
  --data-urlencode 'search=filesystem' --data-urlencode 'version=latest'

# One server: encode the slash in its namespaced name as %2F.
curl -f -sS --connect-timeout 5 --max-time 25 \
  'https://registry.modelcontextprotocol.io/v0/servers/ac.inference.sh%2Fmcp/versions/latest'

# Registry updates since an RFC3339 timestamp, including deleted records.
curl -f -sS --connect-timeout 5 --max-time 25 --get \
  'https://registry.modelcontextprotocol.io/v0/servers' \
  --data-urlencode 'version=latest' --data-urlencode 'updated_since=2026-10-26T00:00:00Z'

The example shows the first two pages; repeat the cursor step using each new response to exhaust the list. Never construct a cursor from a name or version. Keep every filter unchanged across pages and URL-encode the cursor. search matches a substring of the server name. The one-server endpoint is /v0/servers/{encodedName}/versions/{encodedVersion}; use latest or an encoded exact version. /v0/servers/{encodedName}/versions lists that server's versions.

Results are sorted by name and textual version, not semantic version or publication date. Use version=latest and the official latest marker instead of sorting strings to pick a release. updated_since accepts an RFC3339 datetime and automatically includes deleted entries upstream; without it, request include_deleted=true explicitly when lifecycle monitoring matters.

Field pathHow to read it
server.name, server.versionServer identity and the published configuration version.
server.remotes[].type, server.packages[].transport.typePublished remote and package transport types.
server.packages[].registryType, server.repository.urlPackage ecosystem and optional source repository URL.
_meta["io.modelcontextprotocol.registry/official"].isLatestBoolean latest-version marker; it is not inside server.
statusUnder the same official metadata key: active, deprecated or deleted.
publishedAt, updatedAt, statusChangedAtUnder that official key: registry publication, entry update and lifecycle-change timestamps. updatedAt is optional in the published schema. These are not package release dates or your observation time.

Page through all servers with Python 3 and write CSV

Save this standard-library script as registry_watch.py. It follows every cursor with fixed filters, keeps only entries marked latest, and includes active, deprecated and deleted records. CSV contains one row per observed server name, with name, version, transport types, package registry types, repository URL and updated timestamp. Missing optional values are empty; multiple types are sorted and separated by semicolons.

Requests are serial, with a one-second pause before each request, a 20-second socket timeout and an 8 MiB response cap. These are example client limits, not published registry quotas. On HTTP errors, including 429, or invalid responses, the script stops and leaves the previous CSV and state in place. For 429, respect the response's Retry-After before rerunning; for other transient failures, back off and retry later.

# Save as registry_watch.py. Python 3, standard library only.
import csv
import json
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import Request, urlopen

BASE = "https://registry.modelcontextprotocol.io/v0/servers"
OFFICIAL = "io.modelcontextprotocol.registry/official"
DELAY = 1.0  # Serial requests; increase if source policies require it.
CAP = 8 * 1024 * 1024  # Reject a page larger than 8 MiB.

def collect():
    latest = {}
    cursor = None
    seen_cursors = set()
    while True:
        params = {"limit": 100, "version": "latest", "include_deleted": "true"}
        if cursor:
            params["cursor"] = cursor
        request = Request(BASE + "?" + urlencode(params), headers={
            "Accept": "application/json", "User-Agent": "RegistryWatchExample/1.0"})
        time.sleep(DELAY)
        with urlopen(request, timeout=20) as response:
            body = response.read(CAP + 1)
        if len(body) > CAP:
            raise RuntimeError("Oversized page; keep previous state")
        page = json.loads(body)
        entries = page["servers"]
        metadata = page["metadata"]
        if not isinstance(entries, list) or not isinstance(metadata, dict):
            raise ValueError("Unexpected page shape")
        for entry in entries:
            server = entry["server"]
            official = entry["_meta"][OFFICIAL]
            if official["isLatest"] is True:
                latest[server["name"]] = (server, official)
        cursor = metadata.get("nextCursor")
        if not cursor:
            return latest
        if not isinstance(cursor, str) or cursor in seen_cursors:
            raise RuntimeError("Invalid or repeated cursor; keep previous state")
        seen_cursors.add(cursor)

def snapshot(server, official):
    remotes = sorted({(r["type"], r["url"])
                      for r in (server.get("remotes") or [])})
    return {"version": server["version"], "status": official["status"],
            "remotes": [list(r) for r in remotes]}

def main():
    csv_path, state_path = map(Path, sys.argv[1:])
    if csv_path.resolve() == state_path.resolve():
        raise ValueError("CSV and state must be different files")
    prior = json.loads(state_path.read_text(encoding="utf-8")) if state_path.exists() else {}
    previous = prior.get("servers", {})
    latest = collect()  # Any HTTP, timeout or parse error stops before file writes.
    current = {name: snapshot(*entry) for name, entry in latest.items()}
    events = []
    for name, now in sorted(current.items()):
        old = previous.get(name)
        changes = []
        if old is None:
            changes.append("new_server")
        else:
            if old["version"] != now["version"]:
                changes.append("new_version")
            added = set(map(tuple, now["remotes"])) - set(map(tuple, old["remotes"]))
            if added:
                changes.append("new_remotes")
        if (old is None or old["status"] != now["status"]):
            changes.append(now["status"] if now["status"] != "active" else "active")
        if changes:
            events.append({"name": name, "version": now["version"], "changes": changes})
    fields = ["name", "version", "transport_types", "package_registry", "repository_url", "updated"]
    csv_tmp = csv_path.with_name(csv_path.name + ".tmp")
    with csv_tmp.open("w", newline="", encoding="utf-8") as output:
        writer = csv.DictWriter(output, fieldnames=fields)
        writer.writeheader()
        for name, (server, official) in sorted(latest.items()):
            packages = server.get("packages") or []
            transports = {r["type"] for r in (server.get("remotes") or [])}
            transports.update(p["transport"]["type"] for p in packages)
            writer.writerow({
                "name": name, "version": server["version"],
                "transport_types": ";".join(sorted(transports)),
                "package_registry": ";".join(sorted({p["registryType"] for p in packages})),
                "repository_url": (server.get("repository") or {}).get("url", ""),
                "updated": official.get("updatedAt", "")})
    # Preserve missing names: absence from a scan is not proof of deletion.
    next_state = {"observed_at": datetime.now(timezone.utc).isoformat(),
                  "servers": {**previous, **current}}
    state_tmp = state_path.with_name(state_path.name + ".tmp")
    state_tmp.write_text(json.dumps(next_state, indent=2) + "\n", encoding="utf-8")
    csv_tmp.replace(csv_path)
    state_tmp.replace(state_path)
    for event in events:
        print(json.dumps(event))

if __name__ == "__main__":
    main()
python3 registry_watch.py registry.csv registry-state.json

The script only reads the registry API. It does not connect to remotes, follow repository URLs or install packages. Review published URLs and treat CSV fields as untrusted text when opening them in a spreadsheet. Socket timeouts do not impose a total batch deadline. The complete latest inventory is held in memory; a growing registry may need a disk-backed store. Pagination is not a guaranteed point-in-time snapshot, so retain dated exports and reconcile later scans.

Track new servers and updates over time

Keep registry-state.json between runs. The script stores each server's version, lifecycle status and sorted remote type/URL pairs. The first run establishes a baseline and reports new_server for each first observation. That means new to this watch, not newly launched. Later runs print JSON change events for a different latest version (new_version), added remote pairs (new_remotes), and lifecycle changes such as deprecated or deleted. A changed remote type or URL creates a new pair. Version changes are observed differences, not semantic-version comparisons.

Start weekly for discovery or daily for a review queue, adjusting to source policies and your needs. Archive dated CSV files and the printed events. Compare only complete successful scans. The example preserves names missing from a response rather than inferring deletion; explicit lifecycle metadata provides stronger evidence. It does not alert on package-only edits, removed remotes, prose changes or timestamp-only changes.

Each output file is replaced from a temporary file after successful pagination. The CSV and state are separate replacements, so a crash between writes can leave them out of sync. Run one process per state file, retain backups, and use transactional storage and durable event delivery if alerts must survive crashes. Keep state separate for each search or namespace scope if you extend the example.

For a larger watch, first complete a full baseline, then query updated_since with a saved scan-start timestamp and a small overlap. Follow every cursor before advancing the watermark, retain unchanged previous entries, and deduplicate overlapping observations. Do not advance the watermark after a partial or failed scan. Reconcile full scans periodically. The example above deliberately performs a full scan each time; the Actor below implements incremental continuation.

Ways to use a registry change feed

  • Agent platform allow-lists: use discovered names, versions and destinations to propose additions to a reviewed allow-list. Require approval before enabling a server or a new remote.
  • Devtool competitive watch: follow a name substring or namespace and inspect new versions and packaging changes. Registry entries alone do not establish adoption or market share.
  • Security review queues: route new packages, remote changes and deprecations to reviewers. Lifecycle status and metadata flags are triage inputs, not vulnerability findings.
  • Directory sites: publish searchable metadata with source links and observation dates, keep version records distinct, and update lifecycle labels as new evidence arrives.

The easy option: MCP Server Registry Tracker

MCP Server Registry Tracker exports published server versions, remote transports, package coordinates and lifecycle signals from the official registry. Default runs return latest active versions only. Empty input defaults to 200 rows, the preview prefill is 25, and maxResults can be set up to 5,000. Those are collection caps, not registry population counts. Export JSON, CSV or Excel, or consume the dataset through the Apify API.

{
  "search": "filesystem",
  "monitorMode": true,
  "monitorKey": "filesystem-watch",
  "includeDeprecated": true,
  "maxResults": 200
}

Keep monitorKey stable and schedule the same input daily or weekly. Monitor mode always requests latest versions and stores state in the named mcp-registry-tracker-state key-value store. It emits first observations and changes to version, status, sanitized remotes or normalized packages. Prose and timestamp-only edits do not emit rows. Enable includeDeprecated to include deprecated and deleted entries; default active-only filtering hides them.

The README lists a planned price of $2.00 per 1,000 servers, with one server event per emitted dataset row. Pricing is not configured in the source project; the Store page gives the final price. Unchanged and filtered records are not emitted or billed as server rows. Apify platform usage charges may also apply.

The Actor has a 45-second collection budget and a 100-page safety cap; large watches continue across scheduled runs. Check RUN_SUMMARY for stop reason, scan completion and state persistence. A successful run can still be partial or upstream-unavailable. Memory-only state can repeat observations on later runs, and a crash after delivery can duplicate rows. Avoid overlapping runs for one watch. It does not install packages, test endpoints, fetch repository data or perform security assessments.

Frequently asked questions

Is the official MCP Registry a security audit or endorsement?

No. A registry entry describes published server metadata. It does not certify code safety, endpoint behavior or suitability for your agent. Review packages, permissions and remote destinations before approval.

How do I get only the latest MCP server versions?

Send version=latest to /v0/servers and read isLatest under _meta["io.modelcontextprotocol.registry/official"]. Without that filter, results include superseded versions. Textual version ordering does not identify the latest release.

Does new_server mean a server launched today?

No. It means the server name was first observed by this watch. The initial scan establishes a baseline, and older registry entries can be new to your state file. Publication and observation times are different.

How can I track deprecated or deleted MCP servers?

Keep lifecycle status in your state and include deleted entries with include_deleted=true. The API also includes deleted entries automatically for updated_since queries. In MCP Server Registry Tracker, enable includeDeprecated to emit deprecated and deleted rows.

What does MCP Server Registry Tracker cost?

The README lists a planned price of $2.00 per 1,000 servers, with one server event per emitted dataset row. Pricing is not configured in the source project; the Store page gives the final price. Apify platform usage charges may also apply.

Sources

The check labels use this guide's editorial review date. API shapes and field names were verified with read-only GET requests to the official registry's OpenAPI specification, list endpoint and single-version endpoint on October 26, 2026. The documentation and GitHub links below are reference links and were not fetched during preparation. Actor behavior and pricing follow the supplied README, DEVELOPMENT.md and LIVE_TEST_RESULTS.md; no registry-wide count or adoption estimate is claimed.

Actor fields, monitoring limits and planned pricing follow the supplied documentation for MCP Server Registry Tracker.