- Replace build_server_card() with the ServerCard.from_server() classmethod,
matching the SDK's from_* alternate-constructor idiom; the _ServerIdentity
protocol moves to mcp.shared.experimental.server_card alongside it.
- Make DiscoveryResult iterable over its listings (__iter__/__len__), so
'for listing in result:' works without the .listings attribute hop.
- Remove the client-side server_card_url() helper: card URLs must come from
an AI Catalog entry per the discovery spec, never be constructed by the
client. fetch_server_card's docstring now says so.
- Explain the RFC 6598 shared address space constant in the SSRF guard and
rename it _CGNAT_NETWORK -> _SHARED_ADDRESS_SPACE; ipaddress reports these
addresses as neither private nor global, so the guard names them explicitly.
All symbols are experimental (no deprecation cycle), so the removals are clean.
Discovery client:
- Bound each probe with DiscoveryPolicy.max_probe_entries (default 500),
an aggregate budget over every card and nested-catalog entry one walk
processes; exhaustion records a single "probe_budget" failure and drops
the rest. Already-visited nested catalog URLs are never refetched, so
cyclic or duplicated catalogs terminate. Without this the per-catalog
entry cap and the depth cap compose multiplicatively (~entries**depth
fetches from one hostile catalog).
- Catch OSError per entry too: an unresolvable host (socket.gaierror from
the guard's own DNS resolution) or a tar-pit entry (TimeoutError from
the per-fetch deadline) becomes a failure instead of killing the whole
probe, as the DiscoveryResult contract promises. Both exceptions are
now documented on the public fetchers.
- CardListing.listing_domain/hosting_domain return host[:port] built from
the parsed hostname, never the raw netloc, and _admit_url rejects
userinfo-carrying URLs outright, so https://github.com@evil.example/
can neither be fetched nor rendered as a trusted brand in consent UI.
- _is_blocked_address unwraps IPv4-mapped IPv6 literals and applies the
IPv4 rules, closing the ::ffff:100.64.0.1 CGNAT bypass (and the
private-range leak on interpreters without the gh-113171 fix).
- discover_server_cards with http_client=None opens one credential-free
client for the whole walk instead of one per fetched entry.
- Plain http is refused up front under the hardened policy (the loopback
carve-out was unreachable: loopback fails the address guard anyway).
- DiscoveryErrorReason is re-exported from the public module, and one
accept_header() helper replaces the duplicated Accept literals.
Models and server:
- Remote.required_variables now includes required headers that carry no
value and no default, keyed by header name exactly as resolve_remote
accepts them, so prompting from the property and then resolving works.
- mount_discovery documents that public_url is the app's public base URL.
- The discovery routes' CORSMiddleware allows only GET, so real browser
preflights advertise the spec's method list.
Tests cover the budget walk (exact fetch sequence), visited-set dedup,
per-entry DNS-failure and timeout resilience, userinfo rejection and
display, mapped-IPv6 blocking, required-header prompting, Repository and
icons round-trips, and the preflight headers.
New Advanced page covering serving a card, publishing on a brand
domain, static publishing, discovery and connect, ETag revalidation,
and the security model (advisory cards, endpoint-keyed dedup, host
scoped consent, SSRF policy defaults). Tutorials are pyright-checked
docs_src modules proved against the real SDK by
tests/docs_src/test_server_cards.py.