Skip to content

Commit ffce78e

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; the client still accepts the legacy `application/mcp-server+json` as an alias on ingest. - Derive AI Catalog entry identifiers as `urn:air:{publisher}:{name}` (ADR 0015): 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 ffce78e

14 files changed

Lines changed: 128 additions & 236 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Experimental client features.
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
5+
Import directly from submodules:
6+
7+
* ``mcp.client.experimental.server_card`` — fetch and discover Server Cards.
8+
* ``mcp.client.experimental.ai_catalog`` — ingest an AI Catalog.
9+
"""

src/mcp/client/experimental/server_card.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
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"})
37+
# The canonical Server Card media type is ``application/mcp-server-card+json``
38+
# (the MCP discovery extension). The broader AI Catalog specification has
39+
# historically used ``application/mcp-server+json``; accept that as an alias so
40+
# we can ingest catalogs that predate the rename.
41+
_SERVER_CARD_MEDIA_TYPES = frozenset({MCP_SERVER_CARD_MEDIA_TYPE, "application/mcp-server+json"})
4042

4143

4244
async def fetch_server_card(url: str, *, http_client: httpx.AsyncClient | None = None) -> ServerCard:
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""Server-side experimental features.
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
5+
Import directly from submodules:
6+
7+
* ``mcp.server.experimental.server_card`` — build and serve a Server Card.
8+
* ``mcp.server.experimental.ai_catalog`` — build and serve an AI Catalog.
9+
"""

src/mcp/server/experimental/ai_catalog.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
card = build_server_card(server, name="io.modelcontextprotocol.examples/dice-roller")
1313
1414
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")])
15+
mount_server_card(app, card) # GET /server-card
16+
catalog = AICatalog(entries=[server_card_entry(card, "https://dice.example.com/server-card")])
1717
mount_ai_catalog(app, catalog) # GET /.well-known/ai-catalog.json
1818
1919
To write a catalog to a file instead, serialize it with
@@ -29,9 +29,9 @@
2929

3030
from mcp.shared.experimental.ai_catalog.types import (
3131
AI_CATALOG_MEDIA_TYPE,
32+
AI_CATALOG_URN_PREFIX,
3233
AI_CATALOG_WELL_KNOWN_PATH,
3334
MCP_SERVER_CARD_MEDIA_TYPE,
34-
MCP_SERVER_URN_PREFIX,
3535
AICatalog,
3636
CatalogEntry,
3737
)
@@ -50,16 +50,30 @@
5050
}
5151

5252

53+
def _air_identifier(card_name: str) -> str:
54+
"""Derive an AI Catalog ``urn:air:`` identifier from a Server Card name.
55+
56+
The card ``name`` is ``namespace/suffix`` in reverse-DNS form (e.g.
57+
``com.example/weather``); ADR 0015 anchors the identifier on the publisher's
58+
forward-DNS domain, so the namespace labels are reversed
59+
(``com.example`` -> ``example.com``) and the suffix is appended as the name:
60+
``urn:air:example.com:weather``. The optional namespace segment is omitted.
61+
"""
62+
namespace, _, suffix = card_name.partition("/")
63+
publisher = ".".join(reversed(namespace.split(".")))
64+
return f"{AI_CATALOG_URN_PREFIX}{publisher}:{suffix}"
65+
66+
5367
def server_card_entry(card: ServerCard, url: str) -> CatalogEntry:
5468
"""Build the catalog entry advertising ``card``, served at ``url``.
5569
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.
70+
The entry's identifier is derived from the card's ``name`` per AI Catalog
71+
ADR 0015 (``urn:air:{publisher}:{name}``); display name, description and
72+
version are taken from the card. ``url`` should be the absolute URL the card
73+
is retrievable from, since catalogs may be fetched cross-domain.
6074
"""
6175
return CatalogEntry(
62-
identifier=f"{MCP_SERVER_URN_PREFIX}{card.name}",
76+
identifier=_air_identifier(card.name),
6377
display_name=card.title or card.name,
6478
media_type=MCP_SERVER_CARD_MEDIA_TYPE,
6579
url=url,

src/mcp/server/experimental/server_card.py

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
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, serves it (the
6+
discovery extension reserves ``<streamable-http-url>/server-card`` as the
7+
recommended location), and — so clients can actually find it — registers it in
8+
an AI Catalog (see ``mcp.server.experimental.ai_catalog``)::
89
10+
from mcp.server.experimental.ai_catalog import mount_ai_catalog, server_card_entry
911
from mcp.server.experimental.server_card import build_server_card, mount_server_card
12+
from mcp.shared.experimental.ai_catalog import AICatalog
1013
from mcp.shared.experimental.server_card import Remote
1114
1215
card = build_server_card(
@@ -16,9 +19,15 @@
1619
)
1720
1821
app = server.streamable_http_app()
19-
mount_server_card(app, card, path="/server-card.json")
22+
mount_server_card(app, card) # GET /server-card
2023
21-
To write a card to a file instead, serialize it with
24+
# Register the card in an AI Catalog so clients can discover it.
25+
catalog = AICatalog(entries=[server_card_entry(card, "https://dice.example.com/server-card")])
26+
mount_ai_catalog(app, catalog) # GET /.well-known/ai-catalog.json
27+
28+
A Server Card hosted but absent from any catalog is not discoverable: clients
29+
learn a card's URL from an AI Catalog entry rather than guessing it. To write a
30+
card to a file instead of serving it, serialize it with
2231
``card.model_dump_json(by_alias=True, exclude_none=True)``.
2332
"""
2433

@@ -102,14 +111,16 @@ def build_server_card(
102111
)
103112

104113

105-
def server_card_route(card: ServerCard, *, path: str) -> Route:
114+
def server_card_route(card: ServerCard, *, path: str = "/server-card") -> Route:
106115
"""Build a Starlette GET route that serves ``card`` at ``path``.
107116
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.
117+
``path`` defaults to ``/server-card``, matching the location the discovery
118+
extension reserves (``<streamable-http-url>/server-card``). Add the route to
119+
a new app — ``Starlette(routes=[server_card_route(card)])`` — or an existing
120+
one via :func:`mount_server_card`, and advertise the resulting URL in an AI
121+
Catalog entry. The payload is serialized once and served as
122+
``application/mcp-server-card+json`` with the CORS and caching headers
123+
discovery requires.
113124
"""
114125
body = card.model_dump_json(by_alias=True, exclude_none=True).encode()
115126

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

121132

122-
def mount_server_card(app: Starlette, card: ServerCard, *, path: str) -> None:
133+
def mount_server_card(app: Starlette, card: ServerCard, *, path: str = "/server-card") -> None:
123134
"""Attach a Server Card route to an existing Starlette application.
124135
125-
Pre-connection discovery expects the card to be reachable without
126-
authentication; mount it outside any auth middleware.
136+
``path`` defaults to ``/server-card``, the reserved location. Pre-connection
137+
discovery expects the card to be reachable without authentication; mount it
138+
outside any auth middleware.
127139
"""
128140
app.router.routes.append(server_card_route(card, path=path))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Pure experimental MCP features (no server dependencies).
2+
3+
WARNING: These APIs are experimental and may change without notice.
4+
5+
For server-integrated experimental features, use ``mcp.server.experimental``.
6+
"""

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: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,20 @@
3131
#: Media type identifying an AI Catalog document.
3232
AI_CATALOG_MEDIA_TYPE = "application/ai-catalog+json"
3333
#: Media type identifying an MCP Server Card artifact in a catalog entry,
34-
#: per the MCP discovery extension.
35-
MCP_SERVER_CARD_MEDIA_TYPE = "application/mcp-server+json"
34+
#: per the MCP discovery extension. The broader AI Catalog specification has
35+
#: historically used ``application/mcp-server+json``; clients accept that as an
36+
#: alias on ingest (see ``mcp.client.experimental.server_card``).
37+
MCP_SERVER_CARD_MEDIA_TYPE = "application/mcp-server-card+json"
3638
#: Well-known path an AI Catalog is published at, relative to the host root.
3739
AI_CATALOG_WELL_KNOWN_PATH = "/.well-known/ai-catalog.json"
3840
#: Well-known path of the transitional MCP-scoped catalog defined by the MCP
3941
#: discovery extension. Structurally compatible with an AI Catalog.
4042
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:"
43+
#: URN prefix for AI Catalog entry identifiers, per AI Catalog ADR 0015. MCP
44+
#: server entries use ``urn:air:{publisher}:{name}`` where ``publisher`` is the
45+
#: forward-DNS form of the card name's namespace (e.g. ``com.example/weather``
46+
#: -> ``urn:air:example.com:weather``).
47+
AI_CATALOG_URN_PREFIX = "urn:air:"
4348

4449

4550
class TrustSchema(MCPModel):
@@ -179,15 +184,16 @@ class CatalogEntry(MCPModel):
179184
identifier: str
180185
"""Identifier for the artifact; SHOULD be a URN or URI.
181186
182-
MCP server entries use ``urn:mcp:server:<name>`` where ``<name>`` is the
183-
referenced Server Card's ``name``.
187+
MCP server entries use ``urn:air:{publisher}:{name}`` (AI Catalog ADR 0015),
188+
where ``publisher`` is the forward-DNS form of the referenced Server Card's
189+
namespace and ``name`` is its name suffix.
184190
"""
185191

186192
display_name: str
187193
"""Human-readable name for the artifact."""
188194

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

192198
url: str | None = None
193199
"""URL where the full artifact document can be retrieved."""

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)