Add experimental Server Card extension support (SEP-2127)#3139
Draft
claude[bot] wants to merge 5 commits into
Draft
Add experimental Server Card extension support (SEP-2127)#3139claude[bot] wants to merge 5 commits into
claude[bot] wants to merge 5 commits into
Conversation
Shared-tier Pydantic models for the experimental-ext-server-card extension: ServerCard and its Input/KeyValueInput/Repository/Remote family, resolve_remote for template substitution, and a minimal typed AICatalog subset. Wire-format constraints (v1 $schema URL, namespaced name pattern, version-range rejection, url-xor-data catalog entries) are enforced in validators and pinned by the extension repo's vendored conformance fixtures.
Server-tier helpers: build_server_card derives the card from a server's identity fields, route builders and mount helpers serve the card at the spec-reserved <streamable-http-path>/server-card and the catalog at /.well-known/ai-catalog.json, and discovery_response is the single compliance chokepoint (media type, CORS MUSTs, Cache-Control, strong ETag with If-None-Match 304s, OPTIONS preflight). catalog_identifier and server_card_entry build urn:air catalog entries, by URL or inline.
Client-tier helpers: fetch_server_card / fetch_ai_catalog / discover_server_cards run every fetch through a hardened core (https-only with loopback-http exception, SSRF address guard with post-DNS re-check, manual redirect walking with per-hop re-admission, response size and catalog entry/depth caps, Accept and media type discipline) governed by DiscoveryPolicy. Probes collect per-entry failures instead of raising, and CardListing exposes the listing chain for consent UI. Stateless request/parse pairs support host-owned ETag revalidation, and reconcile_server_card compares card claims to runtime values without ever enforcing them.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by David Soria Parra · Slack thread
What this changes
Before: a client had no way to learn about a remote MCP server before connecting. There was no standard place to find its endpoint URLs, the headers it requires, or which servers a domain hosts.
After: a server can publish a Server Card, a JSON document describing its name, remotes, required configuration, and metadata, and can publish an AI Catalog at
/.well-known/ai-cataloglisting the cards its domain offers. A client can discover every card behind a domain in one call, fetch and validate a single card, resolve a remote's URL and headers from user-supplied variable values, and cross-check a fetched card against the live server'sinitializeresult before trusting it.How
All code lives in new
experimentalpackages. Nothing in the existing public API changes, andmcp/__init__.pyis untouched.mcp.shared.experimental.server_cardholds the models: theServerCardfamily (Remote,Repository,Input,KeyValueInput), a typedAICatalogsubset in the siblingmcp.shared.experimental.ai_catalog, and the pureresolve_remote()helper that turns aRemoteplus variable values into a concrete URL and header set with no I/O.mcp.server.experimental.server_cardserves the documents.build_server_card()derives a card from a FastMCP or lowlevel server,mount_server_card(),mount_ai_catalog(), andmount_discovery()attach the routes to a Starlette app, andserver_card_entry()pluscatalog_identifier()build catalog entries. Every response goes through onediscovery_response()chokepoint that applies CORS,Cache-Control, a SHA-256 basedETag,304 Not Modifiedrevalidation, andOPTIONSpreflight handling.mcp.client.experimental.server_cardconsumes them.discover_server_cards()walks a domain's catalog, including nested catalogs, and returns per-entry listings and failures.fetch_server_card()andfetch_ai_catalog()fetch single documents,load_server_card()reads one from disk, and low-levelcreate_*_request()/parse_*_response()pairs support ETag revalidation with a caller-owned HTTP client.reconcile_server_card()compares a card against the server'sinitializeresult. Reconciliation is advisory only. It returns a list ofCardMismatchvalues and never raises, so the caller decides policy.The discovery client is hardened by default under a tunable
DiscoveryPolicy:max_probe_entries) so a hostile catalog tree cannot amplify fan-out. Already-visited catalog URLs are never refetched, so self-referential catalogs terminate after one fetch.DiscoveryFailureand the rest of the walk continues.Spec and prior work
Implements the Server Card extension (SEP-2127), as amended by experimental-ext-server-card#36.
This PR supersedes #2696 and overlaps with the direction of draft #2951, which reached a similar design independently. Ideas from both informed this implementation, and feedback from their authors is very welcome here.
Testing and docs
DiscoveryErrorreason, the address-guard matrix, redirect re-validation, catalog walk edge cases (nesting, cycles, entry and probe budgets), and a full serve, discover, reconcile round trip against a livestreamable_http_app()transport.missing-schema.jsonis deliberately accepted, because ingestion stays lenient about a missing$schemafield by design. A dedicated test pins this divergence.docs/advanced/server-cards.mdwith four runnable tutorial modules underdocs_src/server_cards/, each included in the page and verified by shape tests.Generated by Claude Code