Skip to content

Commit a3912b3

Browse files
committed
Add Server Cards example (SEP-2127)
Adds a self-contained example package under examples/server-card showing how clients can consume and validate an MCP Server Card and how servers can generate and publish one. - mcp_server_card/types.py: Pydantic port of the Server Card / Server schema, following mcp.types conventions (camelCase wire format, reuses Icon). - mcp_server_card/validation.py: JSON Schema validation against the bundled schema plus semantic guards (e.g. rejecting version ranges). - mcp_server_card/client.py: fetch/load + validate a card from the conventional .well-known location. - mcp_server_card/server.py: build a card and either write it to a file or serve it from a Starlette app / MCPServer. - mcp_server_card/cli.py: validate, fetch, and print the schema. - examples/ and tests/ covering both the client and server flows.
1 parent 3eb5799 commit a3912b3

12 files changed

Lines changed: 1917 additions & 0 deletions

File tree

examples/server-card/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# MCP Server Cards — example implementation
2+
3+
A self-contained, runnable example of what **Server Card** ([SEP-2127][sep])
4+
support could look like in the Python SDK. It mirrors the TypeScript source of
5+
truth in [`experimental-ext-server-card`][ext] and follows the SDK's
6+
`mcp.types` conventions, so the `mcp_server_card/` library could be lifted into
7+
`mcp/experimental/server_card/` largely unchanged.
8+
9+
A Server Card is a static metadata document — typically published at
10+
`https://<host>/.well-known/mcp/server-card` — that describes a remote MCP
11+
server's identity, transport endpoints, and supported protocol versions, so a
12+
client can discover and connect to it *before* initialization.
13+
14+
```
15+
mcp_server_card/
16+
types.py # Pydantic models — 1:1 port of schema.ts (camelCase wire format)
17+
schema.json # bundled JSON Schema (from experimental-ext-server-card)
18+
validation.py # JSON Schema + semantic validation -> typed models
19+
client.py # fetch_server_card / load_server_card / well_known_url
20+
server.py # build_server_card / write_server_card / mount_server_card / ...
21+
cli.py # `mcp-server-card` — validate / fetch / schema
22+
examples/
23+
serve_card.py # server: generate, then WRITE a file OR SERVE at .well-known
24+
consume_card.py # client: fetch + validate + act on a card
25+
tests/
26+
test_server_card.py
27+
```
28+
29+
## Design at a glance
30+
31+
**One type port, two consumers.** `types.py` is the only place the schema is
32+
expressed. `Icon` is reused from `mcp.types` (it already exists in the core
33+
spec). The `_meta` and `$schema` fields keep their literal JSON keys via
34+
explicit aliases; everything else is camelCased by the same `to_camel`
35+
generator the rest of the SDK uses.
36+
37+
### Clients: consume + validate
38+
39+
```python
40+
from mcp_server_card import fetch_server_card
41+
42+
# resolves <origin>/.well-known/mcp/server-card, fetches, validates
43+
card = await fetch_server_card("https://dice.example.com")
44+
for remote in card.remotes or []:
45+
print(remote.type, remote.url, remote.supported_protocol_versions)
46+
```
47+
48+
Validation is two layers, both run by `parse_server_card` / `parse_server`:
49+
50+
1. **JSON Schema** against the bundled `schema.json` (the same artifact the
51+
experimental repo validates its examples against) — authoritative structure.
52+
2. **Pydantic** field constraints + semantic guards JSON Schema can't express
53+
(e.g. rejecting version *ranges* like `^1.2.3`).
54+
55+
Failures raise `ServerCardValidationError` carrying every problem at once.
56+
57+
### Servers: generate, then publish
58+
59+
Build the card once from the server's identity, then pick a publishing path:
60+
61+
```python
62+
from mcp_server_card import server_card_from_implementation, streamable_http_remote
63+
64+
card = server_card_from_implementation(
65+
"io.modelcontextprotocol.examples/dice-roller", # reverse-DNS card name
66+
mcp, # an MCPServer (or any Implementation)
67+
remotes=[streamable_http_remote("https://dice.example.com/mcp")],
68+
)
69+
```
70+
71+
- **Write a static file** (publish to a CDN / `.well-known`):
72+
`write_server_card(card, "server-card.json")`
73+
- **Serve from a live MCPServer**: `add_server_card_route(mcp, card)` — adds the
74+
unauthenticated `GET /.well-known/mcp/server-card` route to its Starlette app.
75+
- **Serve from any Starlette app**: `mount_server_card(app, card)`.
76+
77+
## Running
78+
79+
```bash
80+
uv sync
81+
82+
# tests
83+
uv run pytest
84+
85+
# server: write a static card file
86+
uv run python examples/serve_card.py write ./server-card.json
87+
88+
# server: serve it live (Ctrl-C to stop)
89+
uv run python examples/serve_card.py serve --port 8000
90+
91+
# client: fetch + validate it (in another shell)
92+
uv run python examples/consume_card.py http://127.0.0.1:8000
93+
94+
# CLI
95+
uv run mcp-server-card validate ./server-card.json
96+
uv run mcp-server-card fetch http://127.0.0.1:8000
97+
uv run mcp-server-card schema
98+
```
99+
100+
## Notes / open questions
101+
102+
- **Well-known path.** Uses `/.well-known/mcp/server-card` per the experimental
103+
repo's README. The `schema.ts` doc comment says `mcp-server-card` (no
104+
subpath) — worth reconciling upstream. The path is a parameter everywhere.
105+
- **`ServerCard` vs `Server`.** `.well-known` serves `ServerCard` (no
106+
`packages`); the registry `server.json` shape is the `Server` superset, parsed
107+
with `parse_server`.
108+
- **Media type.** Served as `application/json`; SEP-2127 defines no dedicated
109+
media type.
110+
- **Schema distribution.** `schema.json` is bundled for offline validation. A
111+
real SDK integration would track the version published at
112+
`static.modelcontextprotocol.io` and regenerate it the same way the SDK
113+
generates its own types.
114+
115+
[sep]: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2127
116+
[ext]: https://github.com/modelcontextprotocol/experimental-ext-server-card
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Client side: fetch and validate a Server Card, then act on it.
2+
3+
python examples/consume_card.py http://127.0.0.1:8000
4+
5+
Given a server URL (origin or any URL on the host), this resolves the
6+
``.well-known`` location, fetches the card, validates it against the JSON Schema
7+
+ semantic rules, and prints what a client would use to connect.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import asyncio
13+
import sys
14+
15+
from mcp_server_card import ServerCardValidationError, fetch_server_card, well_known_url
16+
17+
18+
async def main(server_url: str) -> int:
19+
print(f"Resolving card: {well_known_url(server_url)}")
20+
try:
21+
card = await fetch_server_card(server_url)
22+
except ServerCardValidationError as exc:
23+
print("Card failed validation:")
24+
for error in exc.errors:
25+
print(f" - {error}")
26+
return 1
27+
28+
print(f"\n{card.title or card.name} ({card.name} v{card.version})")
29+
print(f" {card.description}")
30+
if card.repository:
31+
print(f" source: {card.repository.url}")
32+
for remote in card.remotes or []:
33+
versions = ", ".join(remote.supported_protocol_versions or []) or "unspecified"
34+
print(f" remote [{remote.type}]: {remote.url} (protocols: {versions})")
35+
for header in remote.headers or []:
36+
flags = "required" if header.is_required else "optional"
37+
secret = ", secret" if header.is_secret else ""
38+
print(f" header {header.name} ({flags}{secret})")
39+
return 0
40+
41+
42+
if __name__ == "__main__":
43+
if len(sys.argv) != 2:
44+
print(__doc__)
45+
sys.exit(2)
46+
sys.exit(asyncio.run(main(sys.argv[1])))
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Server side: generate a Server Card, then write it OR serve it.
2+
3+
One card definition, two publishing paths (exactly what the SDK should make easy):
4+
5+
# 1. Hand it to the CLI to publish a static file:
6+
python examples/serve_card.py write ./server-card.json
7+
8+
# 2. Serve it from the live server at /.well-known/mcp/server-card:
9+
python examples/serve_card.py serve --port 8000
10+
11+
The card is derived from the MCPServer's own identity metadata via
12+
``server_card_from_implementation`` and points a remote at this server's
13+
streamable-HTTP endpoint.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import click
19+
import uvicorn
20+
from mcp.server.mcpserver import MCPServer
21+
22+
from mcp_server_card import (
23+
Repository,
24+
ServerCard,
25+
add_server_card_route,
26+
server_card_from_implementation,
27+
streamable_http_remote,
28+
write_server_card,
29+
)
30+
31+
# A normal high-level MCP server with a single tool.
32+
mcp: MCPServer = MCPServer(
33+
name="dice-roller",
34+
title="Dice Roller",
35+
description="Rolls dice for tabletop games.",
36+
version="1.0.0",
37+
website_url="https://example.com/dice",
38+
)
39+
40+
41+
@mcp.tool()
42+
def roll(sides: int = 6) -> int:
43+
"""Roll a single die with the given number of sides."""
44+
return (sides + 1) // 2 # deterministic stand-in so the example stays reproducible
45+
46+
47+
def build_card(public_url: str) -> ServerCard:
48+
"""Build the Server Card for this server, advertising its remote endpoint."""
49+
return server_card_from_implementation(
50+
# Card names are reverse-DNS; the server's display name lives in `title`.
51+
"io.modelcontextprotocol.examples/dice-roller",
52+
mcp,
53+
remotes=[streamable_http_remote(f"{public_url}/mcp", supported_protocol_versions=["2025-11-25"])],
54+
repository=Repository(url="https://github.com/example-org/dice-roller", source="github"),
55+
)
56+
57+
58+
@click.group()
59+
def cli() -> None:
60+
"""Generate, write, or serve the dice-roller Server Card."""
61+
62+
63+
@cli.command()
64+
@click.argument("path", type=click.Path(dir_okay=False))
65+
@click.option("--public-url", default="https://dice.example.com", help="Public origin used in the card's remote URL.")
66+
def write(path: str, public_url: str) -> None:
67+
"""Generate the card and write it to PATH (static publishing)."""
68+
out = write_server_card(build_card(public_url), path)
69+
click.echo(f"Wrote {out}")
70+
71+
72+
@cli.command()
73+
@click.option("--host", default="127.0.0.1")
74+
@click.option("--port", default=8000, type=int)
75+
def serve(host: str, port: int) -> None:
76+
"""Serve the MCP server with its card at /.well-known/mcp/server-card."""
77+
card = build_card(f"http://{host}:{port}")
78+
add_server_card_route(mcp, card) # registers the well-known GET route
79+
app = mcp.streamable_http_app(stateless_http=True, host=host)
80+
click.echo(f"Serving card at http://{host}:{port}/.well-known/mcp/server-card")
81+
uvicorn.run(app, host=host, port=port)
82+
83+
84+
if __name__ == "__main__":
85+
cli()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""MCP Server Cards — example Python implementation (SEP-2127, experimental).
2+
3+
A small, self-contained library showing what Server Card support could look like
4+
in the Python SDK. It mirrors the TypeScript source of truth in
5+
``experimental-ext-server-card/schema.ts`` and follows the SDK's ``mcp.types``
6+
conventions, so the library portion could be lifted into
7+
``mcp/experimental/server_card/`` largely unchanged.
8+
9+
* **Clients** consume and validate a card:
10+
:func:`fetch_server_card`, :func:`load_server_card`, :func:`parse_server_card`.
11+
* **Servers** generate a card and publish it:
12+
:func:`build_server_card`, :func:`write_server_card`, :func:`mount_server_card`,
13+
:func:`add_server_card_route`.
14+
"""
15+
16+
from .client import WELL_KNOWN_PATH, fetch_server_card, load_server_card, well_known_url
17+
from .server import (
18+
add_server_card_route,
19+
build_server_card,
20+
card_to_dict,
21+
card_to_json,
22+
mount_server_card,
23+
server_card_from_implementation,
24+
server_card_route,
25+
streamable_http_remote,
26+
write_server_card,
27+
)
28+
from .types import (
29+
SERVER_CARD_SCHEMA_URL,
30+
SERVER_SCHEMA_URL,
31+
Argument,
32+
Icon,
33+
Input,
34+
InputWithVariables,
35+
KeyValueInput,
36+
NamedArgument,
37+
Package,
38+
PackageTransport,
39+
PositionalArgument,
40+
Remote,
41+
Repository,
42+
Server,
43+
ServerCard,
44+
SsePackageTransport,
45+
StdioTransport,
46+
StreamableHttpPackageTransport,
47+
)
48+
from .validation import (
49+
ServerCardValidationError,
50+
load_bundled_schema,
51+
parse_server,
52+
parse_server_card,
53+
validate_against_schema,
54+
)
55+
56+
__all__ = [
57+
# types
58+
"SERVER_CARD_SCHEMA_URL",
59+
"SERVER_SCHEMA_URL",
60+
"Argument",
61+
"Icon",
62+
"Input",
63+
"InputWithVariables",
64+
"KeyValueInput",
65+
"NamedArgument",
66+
"Package",
67+
"PackageTransport",
68+
"PositionalArgument",
69+
"Remote",
70+
"Repository",
71+
"Server",
72+
"ServerCard",
73+
"SsePackageTransport",
74+
"StdioTransport",
75+
"StreamableHttpPackageTransport",
76+
# client
77+
"WELL_KNOWN_PATH",
78+
"fetch_server_card",
79+
"load_server_card",
80+
"well_known_url",
81+
# server
82+
"add_server_card_route",
83+
"build_server_card",
84+
"card_to_dict",
85+
"card_to_json",
86+
"mount_server_card",
87+
"server_card_from_implementation",
88+
"server_card_route",
89+
"streamable_http_remote",
90+
"write_server_card",
91+
# validation
92+
"ServerCardValidationError",
93+
"load_bundled_schema",
94+
"parse_server",
95+
"parse_server_card",
96+
"validate_against_schema",
97+
]

0 commit comments

Comments
 (0)