Add experimental Server Cards support (SEP-2127) - #2951
Conversation
ffce78e to
e087080
Compare
254b791 to
321181b
Compare
3c4d2b0 to
9f42184
Compare
Adds SDK support for MCP Server Cards: static metadata documents that describe a remote server's identity, transport endpoints, and supported protocol versions for pre-connection discovery. - mcp.shared.experimental.server_card: Pydantic models (ServerCard, Server, Remote, Package, ...) mirroring mcp.types conventions and validating purely through Pydantic. - mcp.server.experimental.server_card: build_server_card derives a card from a server's identity; server_card_route / mount_server_card serve it from a Starlette app at /.well-known/mcp/server-card. - mcp.client.experimental.server_card: fetch_server_card / load_server_card / well_known_url ingest and validate a card. Full test coverage for the new modules.
Server cards are no longer served from a fixed .well-known path. Discovery now goes through an AI Catalog (https://github.com/Agent-Card/ai-catalog) published at /.well-known/ai-catalog.json, whose entries point at server cards hosted anywhere: - Add mcp.shared.experimental.ai_catalog: Pydantic models for the AI Catalog CDDL schema (entries, host, publisher, trust manifest), enforcing the url/data exclusivity and trust-manifest identity binding rules. The transitional MCP Catalog (/.well-known/mcp/catalog.json) is a structural subset and parses with the same models. - Add mcp.server.experimental.ai_catalog: build catalog entries from server cards (urn:mcp:server:<name>) and serve catalogs from the well-known path. - Add discover_server_cards(): fetch a host's catalog (AI Catalog path with fallback to the MCP Catalog path), then fetch or inline-validate every MCP server entry. Non-http(s) card URLs from the catalog are rejected. - Drop WELL_KNOWN_PATH and well_known_url; fetch_server_card now takes the card URL directly and server_card_route/mount_server_card require an explicit path. Review fixes: - Fix the version-range validator rejecting valid semver prereleases like 1.0.0-x; wildcard segments now only count in the release part, and bare "x"/"*" are caught. - Serve discovery documents with the CORS headers the spec requires (MUST) and Cache-Control (SHOULD), exported as DISCOVERY_HEADERS. - Restrict URL resolution to http(s) schemes to match its error message. - Rename httpx_client to http_client and default to create_mcp_http_client() (30s timeout) to match SDK conventions. - Document that lenient ingestion defaults a missing $schema/specVersion, diverging from the JSON Schema's required fields. - Correct the mount_server_card docstring: mounting does not bypass auth middleware. - Add missing test package __init__.py files; assert response headers and bodies in route tests; patch the SDK's own client factory instead of httpx.AsyncClient.
Bring the experimental Server Card support up to date with the latest extension spec (modelcontextprotocol/experimental-ext-server-card) and the AI Catalog discovery docs. This takes over and supersedes modelcontextprotocol#2696, which was stacked on the now-removed Tasks (SEP-1686) work. Conformance fixes: - Pin the Server Card `$schema` to `.../schemas/v1/server-card.schema.json` instead of accepting any `/v1/*.schema.json`; a card referencing the registry `server.schema.json` is now correctly rejected. - Use the canonical artifact media type `application/mcp-server-card+json` when serving and in catalog entries. - Derive AI Catalog entry identifiers as `urn:air:{publisher}:{name}`: the card name's reverse-DNS namespace is turned back into the publisher's forward-DNS domain (`com.example/weather` -> `urn:air:example.com:weather`), replacing the old `urn:mcp:server:` scheme. - Drop the registry-shaped `Server`/`packages` types (and the removed `server.schema.json` reference); v1 is card-only, with locally-runnable package metadata owned by the MCP Registry. `variables` now lives directly on `KeyValueInput`. - Default `server_card_route`/`mount_server_card` to the spec-reserved `/server-card` path. Restore the `experimental/__init__.py` package markers (regular packages, as on the original branch) so `py.typed` propagates and pyright stays clean now that the modules are no longer carried by the Tasks work. Document that a Server Card must be registered in an AI Catalog to be discoverable: clients learn a card's URL from a catalog entry rather than guessing it. Co-authored-by: David Soria Parra <davidsp@anthropic.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Compute strong SHA-256 ETags for server-card and AI Catalog response bodies, handle matching If-None-Match requests with 304 responses, and cover conditional request behavior in tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tadas Antanavicius <tadas@tadasant.com>
Upstream moved MCPModel into the new mcp-types workspace package and turned mcp.types into a versioned package, so the old `mcp.types._types.MCPModel` and `mcp.types.Icon` import paths no longer resolve after rebasing onto main. Import Icon and MCPModel from mcp_types instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: b7d5eea4-c5d2-4401-b7d8-93d0a9b6259a
🤖 Authored via Claude Code, on behalf of @tadasant.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 96e7cd05-3257-4fde-ad04-bf7098028bb6
daaee6a to
0d55856
Compare
Adopt the current AI Catalog entry wire shape and MCP identifier namespace. Remove the retired MCP catalog fallback, keep metadata instability explicit, and tighten the focused tests and documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 13eb8021-1971-4c4f-9d07-4e98c0aaff7b
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/mcp/client/experimental/server_card.py">
<violation number="1" location="src/mcp/client/experimental/server_card.py:49">
P2: A successfully fetched JSON document is accepted regardless of its response `Content-Type`, so this helper does not actually enforce the Server Card media-type contract. Checking the normalized media type after `raise_for_status()` (and advertising only that type in `Accept`) would prevent unrelated JSON endpoints from being treated as Server Cards.</violation>
</file>
<file name="src/mcp/shared/experimental/server_card/types.py">
<violation number="1" location="src/mcp/shared/experimental/server_card/types.py:25">
P2: Generated cards advertise a `$schema` URL that currently returns 404. Schema-aware consumers cannot retrieve the declared Server Card schema; publish it at this URL or update the constant to a served schema URL before release.</violation>
</file>
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
| from pydantic import Field, field_validator | ||
|
|
||
| #: Canonical ``$schema`` value for a Server Card document. | ||
| SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json" |
There was a problem hiding this comment.
P2: Generated cards advertise a $schema URL that currently returns 404. Schema-aware consumers cannot retrieve the declared Server Card schema; publish it at this URL or update the constant to a served schema URL before release.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/shared/experimental/server_card/types.py, line 25:
<comment>Generated cards advertise a `$schema` URL that currently returns 404. Schema-aware consumers cannot retrieve the declared Server Card schema; publish it at this URL or update the constant to a served schema URL before release.</comment>
<file context>
@@ -0,0 +1,164 @@
+from pydantic import Field, field_validator
+
+#: Canonical ``$schema`` value for a Server Card document.
+SERVER_CARD_SCHEMA_URL = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json"
+
+# Pinned to the Server Card schema name: a card referencing the registry
</file context>
There was a problem hiding this comment.
This is intentional and spec-correct, so leaving as-is. The SEP-2127 Server Card spec requires the $schema value to be exactly https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json; that schema document is served from modelcontextprotocol/static once the extension graduates and is simply not published yet (the extension is still pre-release). Hardcoding the spec-mandated URL now is the right thing — and the client tolerates a missing/omitted $schema rather than depending on it resolving.
| if http_client is None: | ||
| async with create_mcp_http_client() as client: | ||
| return await fetch_server_card(url, http_client=client) | ||
| response = await http_client.get(url, headers={"Accept": f"{MCP_SERVER_CARD_MEDIA_TYPE}, application/json"}) |
There was a problem hiding this comment.
P2: A successfully fetched JSON document is accepted regardless of its response Content-Type, so this helper does not actually enforce the Server Card media-type contract. Checking the normalized media type after raise_for_status() (and advertising only that type in Accept) would prevent unrelated JSON endpoints from being treated as Server Cards.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mcp/client/experimental/server_card.py, line 49:
<comment>A successfully fetched JSON document is accepted regardless of its response `Content-Type`, so this helper does not actually enforce the Server Card media-type contract. Checking the normalized media type after `raise_for_status()` (and advertising only that type in `Accept`) would prevent unrelated JSON endpoints from being treated as Server Cards.</comment>
<file context>
@@ -0,0 +1,108 @@
+ if http_client is None:
+ async with create_mcp_http_client() as client:
+ return await fetch_server_card(url, http_client=client)
+ response = await http_client.get(url, headers={"Accept": f"{MCP_SERVER_CARD_MEDIA_TYPE}, application/json"})
+ response.raise_for_status()
+ return ServerCard.model_validate(response.json())
</file context>
There was a problem hiding this comment.
Intentionally lenient, so leaving as-is. The spec treats Server Cards as advisory and explicitly allows hosting them at any URI, including static files/CDNs that serve application/json (see the static-hosting tutorial). Enforcing a Content-Type of application/mcp-server-card+json would reject that spec-endorsed static hosting. We already send Accept: application/mcp-server-card+json, application/json (the spec SHOULD) and validate the JSON body against the Server Card schema, which is the meaningful correctness check.
Fix pre-commit pyright and the failing build_server_card test, and address
the cubic-dev-ai review threads validated against the SEP-2127 discovery spec.
- _ServerIdentity: declare members read-only so the invariant writable-attr
protocol stops rejecting Server.version: str vs version: str | None (pyright).
- build_server_card: treat empty version as unset (Server.version defaults to
"" not None), so the required-version check fires; fix the stale test comment.
- Discovery CORS (spec MUST): allow If-None-Match, expose ETag, and answer the
OPTIONS preflight browsers send before a cross-origin conditional GET; add
"OPTIONS" to both discovery routes; update the docs table and add preflight
tests.
- Docs/tutorials: tutorial001 configures TransportSecuritySettings for the
advertised public host (default localhost protection would 421); tutorial002
creates the output directory before writing; tutorial004 resolves relative
entry URLs against the catalog URL; CatalogEntry.identifier docstring uses the
correct urn:air:{publisher}:mcp:{name} form; clarify the auth-middleware
caveat for globally-applied ASGI middleware.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 23994aca-a562-4387-b723-5c4fd4f9a1ca
Closes modelcontextprotocol/experimental-ext-server-card#16
Summary
Experimental SDK support for MCP Server Cards (SEP-2127) and AI Catalog discovery, opt-in under
mcp.{shared,server,client}.experimental. As an experimental extension, these APIs may change without notice.Takes over #2696 (thanks @dsp-ant — the original two commits are preserved with authorship), rebased cleanly onto current
main. #2696 was stacked on the Tasks (SEP-1686) work, since removed frommain(#2714).Opening as a draft for maintainer review — per the extension repo's contribution rules, AI-assisted changes are not self-merged.
What it does
A server builds a Server Card from its own identity, serves it from a Starlette app, and advertises it through an AI Catalog. A client discovers a host's catalog, then fetches and validates the cards it references. Cards describe identity and remote (HTTP) transport only — locally-runnable package metadata stays in the MCP Registry's
server.json.Conformance to the current schemas
$schemais pinned tohttps://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json; a card pointing at the registry'sserver.schema.jsonis rejected. A document that omits$schemais defaulted to the v1 URL rather than rejected.typefield, requirespecVersion, and accept optionaldisplayName.server_card_entry()emits the minimal MCP shape (identifier,type, andurl) so human-readable fields cannot drift from the referenced card.application/mcp-server-card+json; the client recognizes only this type and skips catalog entries declaring anything else.urn:air:{publisher}:mcp:{name}— the card name's reverse-DNS namespace turned back into the publisher's forward-DNS domain (com.example/weather→urn:air:example.com:mcp:weather)./.well-known/ai-catalog.json. Discovery responses carry the CORS headers the spec requires (MUST),Cache-Control(SHOULD), strong SHA-256 ETags, andIf-None-Matchhandling for conditional requests.versionaccepts exact versions only; ranges and wildcards (^1.2.3,~1.2.3,>=1.2.3,1.x,1 || 2,1 - 2) are rejected.Verification
./scripts/test— full coverage suite andstrict-no-coverpre-commit run --all-files— formatting, lint, pyright, markdownlint, lock consistency, and README snippets200and304, ETags, validation, and client/server helpers🤖 The implementation and cleanup were AI-assisted and human-directed.