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.