Skip to content

Add experimental Server Cards support (SEP-2127) - #2951

Open
SamMorrowDrums wants to merge 11 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:experimental-server-card-v2
Open

Add experimental Server Cards support (SEP-2127)#2951
SamMorrowDrums wants to merge 11 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:experimental-server-card-v2

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Jun 22, 2026

Copy link
Copy Markdown

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 from main (#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

  • $schema is pinned to https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json; a card pointing at the registry's server.schema.json is rejected. A document that omits $schema is defaulted to the v1 URL rather than rejected.
  • AI Catalog entries use the serialized type field, require specVersion, and accept optional displayName. server_card_entry() emits the minimal MCP shape (identifier, type, and url) so human-readable fields cannot drift from the referenced card.
  • Media type — serving and catalog entries use application/mcp-server-card+json; the client recognizes only this type and skips catalog entries declaring anything else.
  • Catalog identifiers are urn:air:{publisher}:mcp:{name} — the card name's reverse-DNS namespace turned back into the publisher's forward-DNS domain (com.example/weatherurn:air:example.com:mcp:weather).
  • Discovery uses the AI Catalog at /.well-known/ai-catalog.json. Discovery responses carry the CORS headers the spec requires (MUST), Cache-Control (SHOULD), strong SHA-256 ETags, and If-None-Match handling for conditional requests.
  • version accepts exact versions only; ranges and wildcards (^1.2.3, ~1.2.3, >=1.2.3, 1.x, 1 || 2, 1 - 2) are rejected.
  • Unsettled AI Catalog metadata remains available but is documented as unstable pending the outcome of Resolve #58: Add ADR-0017 and migrate to namespace extensions map Agent-Card/ai-catalog#77.

Verification

  • ./scripts/test — full coverage suite and strict-no-cover
  • pre-commit run --all-files — formatting, lint, pyright, markdownlint, lock consistency, and README snippets
  • Focused Server Card and AI Catalog tests cover URL and inline discovery, entry filtering, relative URLs, CORS on both 200 and 304, ETags, validation, and client/server helpers

🤖 The implementation and cleanup were AI-assisted and human-directed.

@SamMorrowDrums
SamMorrowDrums force-pushed the experimental-server-card-v2 branch from ffce78e to e087080 Compare June 22, 2026 20:18
Comment thread src/mcp/shared/experimental/ai_catalog/types.py Outdated
Comment thread src/mcp/server/experimental/ai_catalog.py Outdated
dsp-ant and others added 8 commits July 18, 2026 23:06
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
@SamMorrowDrums
SamMorrowDrums force-pushed the experimental-server-card-v2 branch from daaee6a to 0d55856 Compare July 18, 2026 21:13
Comment thread src/mcp/server/experimental/ai_catalog.py Outdated
Comment thread src/mcp/server/experimental/ai_catalog.py

@tadasant tadasant left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a pass at an automated review to ensure alignment with some of the latest Server Card changes --

Comment thread src/mcp/shared/experimental/ai_catalog/types.py Outdated
Comment thread src/mcp/shared/experimental/ai_catalog/types.py Outdated
Comment thread src/mcp/shared/experimental/ai_catalog/types.py Outdated
Comment thread src/mcp/shared/experimental/ai_catalog/types.py
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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread docs_src/server_cards/tutorial001.py Outdated
Comment thread docs/advanced/server-cards.md Outdated
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs_src/server_cards/tutorial002.py
Comment thread docs_src/server_cards/tutorial004.py Outdated
Comment thread src/mcp/server/experimental/ai_catalog.py Outdated
Comment thread src/mcp/shared/experimental/ai_catalog/types.py Outdated
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reference implementations in Python and Go

3 participants