mcp-registry-sdk
Typed, async Python SDK for the MCP Server Registry REST API. Import name:
mcpreg.
Unaffiliated community implementation. This SDK is an independent, third-party project. It
is not affiliated with, endorsed by, or sponsored by Anthropic, the Model Context Protocol
team, or the maintainers of the official registry at registry.modelcontextprotocol.io. "Model
Context Protocol", "MCP" and "MCP Server Registry" refer to the open specification this package
targets; all trademarks remain with their owners. Spec quotations are for interoperability only.
Python is the gap the registry itself acknowledges: its community-clients list covers Go,
TypeScript and Java, and the official mcp package on PyPI is the protocol SDK (JSON-RPC,
transports, tools) — no registry code.
Highlights
| Area | What you get |
|---|---|
| 🧱 Models | All 15 server.json schema definitions, plus the ServerList/ServerResponse envelope. Lenient on parse, strict on construct; unknown fields round-trip. |
| 🔎 Read client | RegistryClient for the three GET endpoints with auto-paging, opaque-cursor handling, and a typed exception hierarchy. |
| ✍️ Write client | PublisherClient for the five write endpoints (POST /publish, PUT/DELETE version, PATCH status ×2). Credentials are a plain headers= dict — no auth class, no token acquisition. |
| 🔄 Sync | The updated_since + cursor loop aggregators otherwise rewrite: high-water mark from _meta.updatedAt, SyncState/SyncStateStore cursor persistence across restarts, reconcile() splitting deleted into a distinct outcome, resumable_scrape() and incremental_pull(). |
| 🛡️ Hardening | Streaming response-body cap (max_response_bytes), method-aware retry (POST/PATCH never retried on a response-level failure; PUT/DELETE are), Retry-After honoured, identiable User-Agent. |
| 🖥️ Server kernel | mcpreg.server — the wire mechanics of answering as a registry: route resolution over percent-encoded paths, query validation, _meta envelope assembly, isLatest ordering, publish-body sanitisation. Pure functions — no ASGI, no framework, no httpx. |
| 🧪 Testing | mcpreg.testing.fixture_client — an httpx.MockTransport-backed stub serving captured responses. Plus InMemorySyncStateStore for trying the sync loop without writing a backend first. |
Python 3.13 or newer is required. The registry itself is still in preview: breaking changes
and data resets are on the table, and this package will stay on 0.x until that changes.
Install
pip install mcp-registry-sdk
# or
uv add mcp-registry-sdk
Schema validation of publish payloads (mcpreg.validation.validate_server_detail) pulls in
jsonschema and is gated behind an optional extra — reading and writing the API never need it:
pip install 'mcp-registry-sdk[registry-tools]'
# or
uv add 'mcp-registry-sdk[registry-tools]'
Read the registry
import asyncio
from mcpreg import RegistryClient
async def main() -> None:
async with RegistryClient() as client:
# One page at a time — the cursor is opaque, pass it back verbatim
page = await client.list_servers(search="filesystem", limit=20)
for entry in page.servers:
print(entry.server.name, entry.status)
# Or walk every page in one go
async for entry in client.iter_servers(search="filesystem"):
print(entry.server.name, entry.server.version, entry.status)
asyncio.run(main())
RegistryClient() with no arguments targets the official instance; point it at any registry
implementing the portable OpenAPI by passing RegistrySettings(base_url=...) or by setting
MCP_REGISTRY_BASE_URL. The client targets the portable spec only — it works against
subregistries and self-hosted instances, not only the official one.
A missing server raises a typed NotFoundError, not a generic HTTPError. Every error retains
the raw response body so the message the registry actually sent is never lost.
Keep an index in sync
The sync layer is the reason this package exists rather than five lines of httpx. A scrape
interrupted mid-run resumes with no re-read and no skip; a server that flips to deleted (a
moderation takedown) is surfaced as a structurally distinct Reconciliation.removals, never
silently filtered.
from mcpreg import RegistryClient
from mcpreg.sync import InMemorySyncStateStore, reconcile, resumable_scrape
# InMemorySyncStateStore is for a first run and for tests — it does NOT survive a process
# restart. Bring your own SyncStateStore (file, database, Redis) for anything unattended.
store = InMemorySyncStateStore()
async with RegistryClient() as client:
async for changes in resumable_scrape(client, store):
for entry in changes.upserts:
index[entry.server.name] = entry
for entry in changes.removals: # status == "deleted"; impossible to ignore by design
index.pop(entry.server.name, None)
incremental_pull() is the steady-state counterpart — it sources updated_since from the saved
high-water mark (never the local clock; clock skew silently drops entries), and forces
include_deleted=True so takedowns reach an incremental consumer at all.
Publish a server
import os
from mcpreg import PublisherClient, RegistrySettings, ServerDetail
server_detail = ServerDetail(
name="io.github.example/weather",
description="A weather server.",
version="1.0.0",
)
async with PublisherClient(
headers={"Authorization": f"Bearer {os.environ['MCP_REGISTRY_TOKEN']}"},
settings=RegistrySettings(base_url="https://registry.example.com"),
) as pub:
await pub.publish(server_detail)
headers= is a plain HTTP concept, not an auth abstraction: the SDK has no opinion about what
scheme a caller uses (Bearer, basic auth, a custom API-key header, none at all for a local
registry). Auth acquisition is out of scope — bring a token you already have.
For read-after-write, share the publisher's connection pool rather than paying for a second one:
async with RegistryClient(settings, http=pub.http) as reader:
current = await reader.get_version(server_detail.name, "latest")
pub.http is borrowed, not owned: RegistryClient will not close it on exit.
Serve a registry
mcpreg.server is the inverse of the client — the wire mechanics of answering as a registry,
for a backend author. Pure functions over the same wire models, no ASGI, no framework, no httpx,
so it composes with FastAPI, Starlette, Litestar or Django with no adapter:
from mcpreg.server import Operation, RegistryError, ListQuery, page, resolve, to_wire
# `request` is whatever framework object carries the method, the raw (still-encoded) path,
# and the query string. Starlette: request.method / request.scope["raw_path"] / request.query_params.
route = resolve("GET", "/v0.1/servers")
if route is None:
raise RegistryError(404, "no such endpoint")
if route.operation is Operation.LIST_SERVERS:
query = ListQuery.from_params({"limit": "10"}) # include_deleted already correct
# `my_entries(query)` is the backend's own lookup — whatever the storage layer is
window: list = []
body = to_wire(page(window, next_cursor=None))
It never stores, authenticates, or verifies namespace ownership — see
docs/04-server.md. The trust model rests on that last one, so an SDK that
appeared to enforce it while actually rubber-stamping would be worse than one that never offered.
Scope
In: the portable OpenAPI's three read endpoints, the five write endpoints, typed models for
all fifteen schema definitions, the incremental-sync loop, registry-author helpers (schema
validation, the error-envelope builder, the write-side wire models), and the mcpreg.server
kernel.
Out: connecting to MCP servers (the mcp package's job), building or running install
commands, curation, and auth acquisition. And — even inside mcpreg.server — storage,
authentication, and namespace-ownership verification: the three things that make a registry a
product rather than a wire format.
Full reasoning in docs/00-scope.md.
Documentation
docs/00-scope.md— what ships and what never willdocs/spec/01-api.md— the endpoint contractdocs/spec/02-data-model.md— the schema, field by fielddocs/spec/REFRESH.md— spec-refresh protocoldocs/03-design.md— module layout, type rules, errors, testingdocs/04-server.md— serving a registry withmcpreg.serverdocs/open-questions.md— open decisions and spec drift
Development
uv sync
uv run pytest # default run: no network
REGISTRY_LIVE=1 uv run pytest -m live # opt-in: hits the real registry
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy src tests
uv run lint-imports
Every Python code sample in this README is executed by tests/test_readme.py, so the README
cannot silently drift from the actual API.
Issue tracking is beads: bd ready for available work.
Release
Releases are built from a clean main commit that has passed CI. The package version and source
tag must agree: version 0.1.0 is tagged v0.1.0.
uv build
uv run --with twine twine check dist/*
uv publish dist/*
After publication, verify the supported boundary from a clean environment by installing the
version range downstream applications will use: mcp-registry-sdk>=0.1,<0.2.
MIT licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_registry_sdk-0.1.0.tar.gz.
File metadata
- Download URL: mcp_registry_sdk-0.1.0.tar.gz
- Upload date:
- Size: 157.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
833a30f1e297a7876f374175cf7840c96d215fba2e0c86de7e17be65c1d2b8c3
|
|
| MD5 |
5ef0a4d1677fdd39264be83a23fc5101
|
|
| BLAKE2b-256 |
f4d76e876cf255a80f54934853b59219731466d992a8f4ff4a6d973b76b9f4b9
|
File details
Details for the file mcp_registry_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_registry_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 81.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
35b4bba1905b31527fcc50cb26e4c77e09d99eeec35e8371acab215783180233
|
|
| MD5 |
33ce5aaaf13779e51a4c6c01ab9176b3
|
|
| BLAKE2b-256 |
506fc427c388492e2f583c927cc1979cb020aeb990aa318e9fe3d8401441dd12
|