Metadata-Version: 2.4
Name: mcp-registry-sdk
Version: 0.1.0
Summary: Typed async Python client for the MCP Server Registry API
Project-URL: Homepage, https://github.com/ukw2d/mcp_registry_sdk
Project-URL: Repository, https://github.com/ukw2d/mcp_registry_sdk
Project-URL: Issues, https://github.com/ukw2d/mcp_registry_sdk/issues
Project-URL: Specification, https://github.com/modelcontextprotocol/registry/blob/main/docs/reference/api/openapi.yaml
License-Expression: MIT
License-File: LICENSE
Keywords: client,mcp,model-context-protocol,registry
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
Requires-Python: >=3.13
Requires-Dist: httpx>=0.28.1
Requires-Dist: pydantic-settings>=2.14.2
Requires-Dist: pydantic>=2.11
Provides-Extra: registry-tools
Requires-Dist: jsonschema>=4.25; extra == 'registry-tools'
Description-Content-Type: text/markdown

# mcp-registry-sdk

[![PyPI](https://img.shields.io/pypi/v/mcp-registry-sdk.svg)](https://pypi.org/project/mcp-registry-sdk/)
[![Python](https://img.shields.io/pypi/pyversions/mcp-registry-sdk.svg)](https://pypi.org/project/mcp-registry-sdk/)
[![CI](https://img.shields.io/github/actions/workflow/status/ukw2d/mcp_registry_sdk/ci.yml?branch=main&label=CI)](https://github.com/ukw2d/mcp_registry_sdk/actions/workflows/ci.yml)
[![code style: Ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://docs.astral.sh/ruff/)
[![typed: mypy strict](https://img.shields.io/badge/typed-mypy%20strict-blue.svg)](https://mypy-lang.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![spec target](https://img.shields.io/badge/targets-MCP%20Registry%20v0.1-orange.svg)](#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

```bash
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:

```bash
pip install 'mcp-registry-sdk[registry-tools]'
# or
uv add 'mcp-registry-sdk[registry-tools]'
```

## Read the registry

```python
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.

```python
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

```python
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:

```python
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:

```python
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`](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`](docs/00-scope.md).

## Documentation

- [`docs/00-scope.md`](docs/00-scope.md) — what ships and what never will
- [`docs/spec/01-api.md`](docs/spec/01-api.md) — the endpoint contract
- [`docs/spec/02-data-model.md`](docs/spec/02-data-model.md) — the schema, field by field
- [`docs/spec/REFRESH.md`](docs/spec/REFRESH.md) — spec-refresh protocol
- [`docs/03-design.md`](docs/03-design.md) — module layout, type rules, errors, testing
- [`docs/04-server.md`](docs/04-server.md) — serving a registry with `mcpreg.server`
- [`docs/open-questions.md`](docs/open-questions.md) — open decisions and spec drift

## Development

```bash
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](https://github.com/steveyegge/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`.

```bash
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.
