If you want to know what is actually in the official Model Context Protocol (MCP) registry, you can click through 55,000 pages, or you can load one file. I keep a dataset of the registry's entries, and on 2026-11-04 I published the 2026-11-03 version on Zenodo, Hugging Face and Kaggle. This post shows how to load it and what to watch for. Every snippet below was run before I wrote this, and every number is what it printed.
What is in it
-
servers.csv: one row per server, 22 columns (name, hosting type, transport, registry type, licence, maintenance, repository, downloads last month, GitHub stars, number of tools listed, and more). 55,773 rows. -
servers-part-1..6.ndjson.gz: the full record per server, 10,000 per part, the same JSON the ProtocolProbe API serves. -
version-history.ndjson.gz,tool-clone-groups.json,SHA256SUMS,CITATION.cff.
Licence: CC BY 4.0. It holds facts read from public records and no security verdict for any server.
Way 1: pandas, straight from Hugging Face
import pandas as pd
base = "https://huggingface.co/datasets/sultan57/mcp-registry-facts/resolve/main/"
df = pd.read_csv(base + "servers.csv")
print(df.shape) # (55773, 22)
print(df.hosting_type.value_counts(dropna=False))
That gave: remote 32,873, local-package 17,847, both 4,389, and 664 rows where the entry declares neither a package nor a remote. So 58.9% of the entries are remote servers you reach over HTTP, not something you install.
Way 2: the full records
The CSV is the summary. The JSON-lines parts keep everything the API has:
import gzip, io, json, urllib.request
req = urllib.request.Request(base + "servers-part-6.ndjson.gz", headers={"User-Agent": "me"})
raw = urllib.request.urlopen(req, timeout=120).read()
rows = [json.loads(line) for line in gzip.open(io.BytesIO(raw), "rt", encoding="utf-8")]
print(len(rows), sorted(rows[0])[:6]) # 5773 ['asOf', 'attribution', 'deps', 'depsLatest', 'derived', 'language']
Each part is a complete gzip file, so read them in order for the full list. Kaggle unpacks the .gz files, so there you get plain .ndjson.
Way 3: the archived copy with a DOI
If you cite the data, cite the version you used: 10.5281/zenodo.23943460 is the 2026-11-03 version, and the concept DOI 10.5281/zenodo.23386799 always resolves to the newest.
Check the file you got
SHA256SUMS lists the data files. This confirms the CSV is byte for byte the published one:
import hashlib
def get(name):
r = urllib.request.Request(base + name, headers={"User-Agent": "me"})
return urllib.request.urlopen(r, timeout=120).read()
sums = {l.split()[1].lstrip("*"): l.split()[0] for l in get("SHA256SUMS").decode().splitlines() if l.strip()}
print(hashlib.sha256(get("servers.csv")).hexdigest() == sums["servers.csv"]) # True
Two traps
1. An empty cell means "not stated", never "no". license_class is empty for 37,191 rows. Remote servers ship no package, so there is no package licence to read; that says nothing about the server's licence. If you filter on a licence, you are filtering on the package half of the registry:
x = df[(df.maintenance == "active") & (df.license_class == "permissive")]
print(len(x)) # 12056, and none of them is a remote-only server
2. Counts of tools are partial. tools_listed is filled for 26,261 servers (the ones whose tool list we could read without calling any tool); the median is 8 tools and the largest list is 1,711. A server without a number was not read, which is different from having no tools.
What I would not do with it
Do not rank servers by a column and call the top ones good. The columns are facts (is the repository archived, is there a licence, how many downloads), not a judgement, and the AI-classified fields (topic, tool kinds) were read by an AI model, with two differently worded runs having to agree, not checked by hand.
If a fact about your own server is wrong, tell me at hello@protocolprobe.com and I will re-check it.
Links: Hugging Face, Kaggle, how to cite.
Top comments (0)