Skip to content

Commit 254b791

Browse files
SamMorrowDrumsdsp-antCopilot
committed
Align experimental Server Cards with SEP-2127 v1 schema
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 #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>
1 parent da0e0a8 commit 254b791

15 files changed

Lines changed: 141 additions & 293 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Experimental client-side MCP features.
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
"""

src/mcp/client/experimental/server_card.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
print(remote.type, remote.url, remote.supported_protocol_versions)
1313
1414
Returned :class:`ServerCard` objects are validated; malformed documents raise
15-
``pydantic.ValidationError``. Ingestion is deliberately lenient about a
16-
missing ``$schema`` key — see ``ServerCard.schema_uri``.
15+
``pydantic.ValidationError``. A missing ``$schema`` key is tolerated — see
16+
``ServerCard.schema_uri``.
1717
"""
1818

1919
from __future__ import annotations
@@ -34,10 +34,6 @@
3434

3535
__all__ = ["fetch_server_card", "load_server_card", "discover_server_cards"]
3636

37-
# The MCP discovery extension and the AI Catalog specification currently name
38-
# the Server Card media type differently; accept either when filtering.
39-
_SERVER_CARD_MEDIA_TYPES = frozenset({MCP_SERVER_CARD_MEDIA_TYPE, "application/mcp-server-card+json"})
40-
4137

4238
async def fetch_server_card(url: str, *, http_client: httpx.AsyncClient | None = None) -> ServerCard:
4339
"""Fetch and validate the Server Card at ``url``.
@@ -95,7 +91,7 @@ async def discover_server_cards(url: str, *, http_client: httpx.AsyncClient | No
9591

9692
cards: list[ServerCard] = []
9793
for entry in catalog.entries:
98-
if entry.media_type not in _SERVER_CARD_MEDIA_TYPES:
94+
if entry.media_type != MCP_SERVER_CARD_MEDIA_TYPE:
9995
continue
10096
if entry.url is not None:
10197
# Entry URLs are usually absolute; resolve relative ones against
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Experimental server-side MCP features.
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
"""

src/mcp/server/experimental/ai_catalog.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,13 @@
22
33
WARNING: These APIs are experimental and may change without notice.
44
5-
A server author advertises their MCP server by serving an AI Catalog from the
6-
well-known path, with an entry pointing at the server's Server Card::
5+
A server advertises its MCP server(s) by serving an AI Catalog from the
6+
well-known path, with one entry per Server Card::
77
8-
from mcp.server.experimental.ai_catalog import mount_ai_catalog, server_card_entry
9-
from mcp.server.experimental.server_card import build_server_card, mount_server_card
10-
from mcp.shared.experimental.ai_catalog import AICatalog
8+
catalog = AICatalog(entries=[server_card_entry(card, "https://example.com/server-card")])
9+
mount_ai_catalog(server.streamable_http_app(), catalog) # GET /.well-known/ai-catalog.json
1110
12-
card = build_server_card(server, name="io.modelcontextprotocol.examples/dice-roller")
13-
14-
app = server.streamable_http_app()
15-
mount_server_card(app, card, path="/server-card.json")
16-
catalog = AICatalog(entries=[server_card_entry(card, "https://dice.example.com/server-card.json")])
17-
mount_ai_catalog(app, catalog) # GET /.well-known/ai-catalog.json
18-
19-
To write a catalog to a file instead, serialize it with
11+
To write a catalog to a file instead, use
2012
``catalog.model_dump_json(by_alias=True, exclude_none=True)``.
2113
"""
2214

@@ -29,9 +21,9 @@
2921

3022
from mcp.shared.experimental.ai_catalog.types import (
3123
AI_CATALOG_MEDIA_TYPE,
24+
AI_CATALOG_URN_PREFIX,
3225
AI_CATALOG_WELL_KNOWN_PATH,
3326
MCP_SERVER_CARD_MEDIA_TYPE,
34-
MCP_SERVER_URN_PREFIX,
3527
AICatalog,
3628
CatalogEntry,
3729
)
@@ -40,8 +32,8 @@
4032
__all__ = ["DISCOVERY_HEADERS", "server_card_entry", "ai_catalog_route", "mount_ai_catalog"]
4133

4234
#: Response headers for discovery endpoints (catalogs and the artifacts they
43-
#: reference). Browser-based clients must be able to read them: the discovery
44-
#: spec makes the CORS headers a MUST and the caching header a SHOULD.
35+
#: reference): CORS headers so browser clients can read them, plus a caching
36+
#: hint.
4537
DISCOVERY_HEADERS = {
4638
"Access-Control-Allow-Origin": "*",
4739
"Access-Control-Allow-Methods": "GET",
@@ -50,16 +42,29 @@
5042
}
5143

5244

45+
def _air_identifier(card_name: str) -> str:
46+
"""Derive an AI Catalog ``urn:air:`` identifier from a Server Card name.
47+
48+
The card ``name`` is ``namespace/suffix`` in reverse-DNS form
49+
(``com.example/weather``); the namespace labels are reversed to forward-DNS
50+
(``com.example`` -> ``example.com``) and the suffix appended:
51+
``urn:air:example.com:weather``.
52+
"""
53+
namespace, _, suffix = card_name.partition("/")
54+
publisher = ".".join(reversed(namespace.split(".")))
55+
return f"{AI_CATALOG_URN_PREFIX}{publisher}:{suffix}"
56+
57+
5358
def server_card_entry(card: ServerCard, url: str) -> CatalogEntry:
5459
"""Build the catalog entry advertising ``card``, served at ``url``.
5560
56-
The entry's identifier is derived from the card's ``name`` per the MCP
57-
discovery extension (``urn:mcp:server:<name>``); display name, description
58-
and version are taken from the card. ``url`` should be the absolute URL
59-
the card is retrievable from, since catalogs may be fetched cross-domain.
61+
The entry's identifier is derived from the card's ``name``
62+
(``urn:air:{publisher}:{name}``); display name, description and version are
63+
taken from the card. ``url`` should be the absolute URL the card is
64+
retrievable from, since catalogs may be fetched cross-domain.
6065
"""
6166
return CatalogEntry(
62-
identifier=f"{MCP_SERVER_URN_PREFIX}{card.name}",
67+
identifier=_air_identifier(card.name),
6368
display_name=card.title or card.name,
6469
media_type=MCP_SERVER_CARD_MEDIA_TYPE,
6570
url=url,

src/mcp/server/experimental/server_card.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,21 @@
22
33
WARNING: These APIs are experimental and may change without notice.
44
5-
A server author builds a card from the server's identity and serves it at a
6-
path of their choosing, advertised through an AI Catalog (see
7-
``mcp.server.experimental.ai_catalog``)::
5+
A server author builds a card from the server's identity and serves it. The
6+
recommended location is ``<streamable-http-url>/server-card`` — pass ``path`` to
7+
match the MCP endpoint (e.g. ``/mcp/server-card`` when the server is mounted at
8+
``/mcp``)::
89
9-
from mcp.server.experimental.server_card import build_server_card, mount_server_card
10-
from mcp.shared.experimental.server_card import Remote
10+
card = build_server_card(server, name="com.example/dice-roller", remotes=[...])
11+
mount_server_card(server.streamable_http_app(), card, path="/mcp/server-card")
1112
12-
card = build_server_card(
13-
server,
14-
name="io.modelcontextprotocol.examples/dice-roller",
15-
remotes=[Remote(type="streamable-http", url="https://dice.example.com/mcp")],
16-
)
17-
18-
app = server.streamable_http_app()
19-
mount_server_card(app, card, path="/server-card.json")
13+
Clients learn the card's URL from a catalog entry, so any reachable path works;
14+
the convention only matters for fallback probing.
2015
21-
To write a card to a file instead, serialize it with
22-
``card.model_dump_json(by_alias=True, exclude_none=True)``.
16+
A hosted card is only discoverable once it is registered in an AI Catalog (see
17+
``mcp.server.experimental.ai_catalog``); clients learn a card's URL from a
18+
catalog entry rather than guessing it. To write a card to a file instead of
19+
serving it, use ``card.model_dump_json(by_alias=True, exclude_none=True)``.
2320
"""
2421

2522
from __future__ import annotations
@@ -102,14 +99,16 @@ def build_server_card(
10299
)
103100

104101

105-
def server_card_route(card: ServerCard, *, path: str) -> Route:
102+
def server_card_route(card: ServerCard, *, path: str = "/server-card") -> Route:
106103
"""Build a Starlette GET route that serves ``card`` at ``path``.
107104
108-
Add it to a new app — ``Starlette(routes=[server_card_route(card, path=...)])``
109-
— or an existing one via :func:`mount_server_card`, and advertise the
110-
resulting URL in an AI Catalog entry. The payload is serialized once and
111-
served as ``application/mcp-server+json`` with the CORS and caching
112-
headers discovery requires.
105+
``path`` defaults to ``/server-card``, the recommended location
106+
(``<streamable-http-url>/server-card``). Add the route to
107+
a new app — ``Starlette(routes=[server_card_route(card)])`` — or an existing
108+
one via :func:`mount_server_card`, and advertise the resulting URL in an AI
109+
Catalog entry. The payload is serialized once and served as
110+
``application/mcp-server-card+json`` with the CORS and caching headers
111+
discovery requires.
113112
"""
114113
body = card.model_dump_json(by_alias=True, exclude_none=True).encode()
115114

@@ -119,10 +118,11 @@ async def endpoint(_request: Request) -> Response:
119118
return Route(path, endpoint=endpoint, methods=["GET"], name="mcp_server_card")
120119

121120

122-
def mount_server_card(app: Starlette, card: ServerCard, *, path: str) -> None:
121+
def mount_server_card(app: Starlette, card: ServerCard, *, path: str = "/server-card") -> None:
123122
"""Attach a Server Card route to an existing Starlette application.
124123
125-
Pre-connection discovery expects the card to be reachable without
126-
authentication; mount it outside any auth middleware.
124+
``path`` defaults to ``/server-card``, the reserved location. Pre-connection
125+
discovery expects the card to be reachable without authentication; mount it
126+
outside any auth middleware.
127127
"""
128128
app.router.routes.append(server_card_route(card, path=path))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Shared experimental MCP features.
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
"""

src/mcp/shared/experimental/ai_catalog/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313
from mcp.shared.experimental.ai_catalog.types import (
1414
AI_CATALOG_MEDIA_TYPE,
15+
AI_CATALOG_URN_PREFIX,
1516
AI_CATALOG_WELL_KNOWN_PATH,
1617
MCP_CATALOG_WELL_KNOWN_PATH,
1718
MCP_SERVER_CARD_MEDIA_TYPE,
18-
MCP_SERVER_URN_PREFIX,
1919
AICatalog,
2020
Attestation,
2121
CatalogEntry,
@@ -28,10 +28,10 @@
2828

2929
__all__ = [
3030
"AI_CATALOG_MEDIA_TYPE",
31+
"AI_CATALOG_URN_PREFIX",
3132
"AI_CATALOG_WELL_KNOWN_PATH",
3233
"MCP_CATALOG_WELL_KNOWN_PATH",
3334
"MCP_SERVER_CARD_MEDIA_TYPE",
34-
"MCP_SERVER_URN_PREFIX",
3535
"AICatalog",
3636
"Attestation",
3737
"CatalogEntry",

src/mcp/shared/experimental/ai_catalog/types.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@
3232
AI_CATALOG_MEDIA_TYPE = "application/ai-catalog+json"
3333
#: Media type identifying an MCP Server Card artifact in a catalog entry,
3434
#: per the MCP discovery extension.
35-
MCP_SERVER_CARD_MEDIA_TYPE = "application/mcp-server+json"
35+
MCP_SERVER_CARD_MEDIA_TYPE = "application/mcp-server-card+json"
3636
#: Well-known path an AI Catalog is published at, relative to the host root.
3737
AI_CATALOG_WELL_KNOWN_PATH = "/.well-known/ai-catalog.json"
3838
#: Well-known path of the transitional MCP-scoped catalog defined by the MCP
3939
#: discovery extension. Structurally compatible with an AI Catalog.
4040
MCP_CATALOG_WELL_KNOWN_PATH = "/.well-known/mcp/catalog.json"
41-
#: URN prefix for MCP server entry identifiers (``urn:mcp:server:<name>``).
42-
MCP_SERVER_URN_PREFIX = "urn:mcp:server:"
41+
#: URN prefix for AI Catalog entry identifiers. MCP server entries use
42+
#: ``urn:air:{publisher}:{name}`` where ``publisher`` is the forward-DNS form of
43+
#: the card name's namespace (``com.example/weather`` -> ``urn:air:example.com:weather``).
44+
AI_CATALOG_URN_PREFIX = "urn:air:"
4345

4446

4547
class TrustSchema(MCPModel):
@@ -179,15 +181,16 @@ class CatalogEntry(MCPModel):
179181
identifier: str
180182
"""Identifier for the artifact; SHOULD be a URN or URI.
181183
182-
MCP server entries use ``urn:mcp:server:<name>`` where ``<name>`` is the
183-
referenced Server Card's ``name``.
184+
MCP server entries use ``urn:air:{publisher}:{name}``, where ``publisher`` is
185+
the forward-DNS form of the referenced Server Card's namespace and ``name``
186+
is its name suffix.
184187
"""
185188

186189
display_name: str
187190
"""Human-readable name for the artifact."""
188191

189192
media_type: str
190-
"""Media type identifying the artifact type (e.g. ``"application/mcp-server+json"``)."""
193+
"""Media type identifying the artifact type (e.g. ``"application/mcp-server-card+json"``)."""
191194

192195
url: str | None = None
193196
"""URL where the full artifact document can be retrieved."""
@@ -241,8 +244,7 @@ class AICatalog(MCPModel):
241244
spec_version: str = "1.0"
242245
"""The AI Catalog specification version, in ``"Major.Minor"`` format.
243246
244-
The specification marks ``specVersion`` as required; ingestion here is
245-
deliberately lenient and defaults it for documents that omit the key.
247+
Required by the specification; defaulted here for documents that omit it.
246248
"""
247249

248250
entries: list[CatalogEntry]

src/mcp/shared/experimental/server_card/__init__.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,20 @@
1212

1313
from mcp.shared.experimental.server_card.types import (
1414
SERVER_CARD_SCHEMA_URL,
15-
SERVER_SCHEMA_URL,
16-
Argument,
1715
Icon,
1816
Input,
19-
InputWithVariables,
2017
KeyValueInput,
21-
NamedArgument,
22-
Package,
23-
PackageTransport,
24-
PositionalArgument,
2518
Remote,
2619
Repository,
27-
Server,
2820
ServerCard,
29-
SsePackageTransport,
30-
StdioTransport,
31-
StreamableHttpPackageTransport,
3221
)
3322

3423
__all__ = [
3524
"SERVER_CARD_SCHEMA_URL",
36-
"SERVER_SCHEMA_URL",
37-
"Argument",
3825
"Icon",
3926
"Input",
40-
"InputWithVariables",
4127
"KeyValueInput",
42-
"NamedArgument",
43-
"Package",
44-
"PackageTransport",
45-
"PositionalArgument",
4628
"Remote",
4729
"Repository",
48-
"Server",
4930
"ServerCard",
50-
"SsePackageTransport",
51-
"StdioTransport",
52-
"StreamableHttpPackageTransport",
5331
]

0 commit comments

Comments
 (0)