Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/mcp/client/experimental/server_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Ingest MCP Server Cards (SEP-2127).

WARNING: These APIs are experimental and may change without notice.

A client discovers how to connect to a remote server by fetching its card from
the conventional ``.well-known`` location before initializing a session::

from mcp.client.experimental.server_card import fetch_server_card

card = await fetch_server_card("https://dice.example.com")
for remote in card.remotes or []:
print(remote.type, remote.url, remote.supported_protocol_versions)

The returned :class:`ServerCard` is fully validated; malformed documents raise
``pydantic.ValidationError``.
"""

from __future__ import annotations

import json
from pathlib import Path
from urllib.parse import urljoin, urlsplit

import httpx

from mcp.shared.experimental.server_card.types import WELL_KNOWN_PATH, ServerCard

__all__ = ["well_known_url", "fetch_server_card", "load_server_card"]


def well_known_url(url: str, *, well_known_path: str = WELL_KNOWN_PATH) -> str:
"""Resolve the Server Card URL for a server's origin.

Accepts either a bare origin (``https://example.com``) or any URL on the
server (e.g. its ``/mcp`` endpoint); the card always lives at the host root.

Raises:
ValueError: If ``url`` is not an absolute http(s) URL.
"""
parts = urlsplit(url)
if not parts.scheme or not parts.netloc:
raise ValueError(f"Expected an absolute http(s) URL, got {url!r}")
origin = f"{parts.scheme}://{parts.netloc}"
return urljoin(origin, well_known_path)


async def fetch_server_card(
url: str,
*,
well_known_path: str = WELL_KNOWN_PATH,
httpx_client: httpx.AsyncClient | None = None,
) -> ServerCard:
"""Fetch and validate the Server Card for the server at ``url``.

``url`` may be the server's origin or any URL on the same host; the card is
resolved to ``<origin><well_known_path>``. Pass an existing ``httpx_client``
to reuse connection pooling / auth, otherwise a short-lived client is used.

Raises:
ValueError: If ``url`` is not an absolute http(s) URL.
httpx.HTTPError: If the request fails or returns a non-2xx status.
pydantic.ValidationError: If the document is not a valid Server Card.
"""
target = well_known_url(url, well_known_path=well_known_path)

if httpx_client is None:
async with httpx.AsyncClient(follow_redirects=True) as client:
response = await client.get(target, headers={"Accept": "application/json"})
else:
response = await httpx_client.get(target, headers={"Accept": "application/json"})
response.raise_for_status()
return ServerCard.model_validate(response.json())


def load_server_card(path: str | Path) -> ServerCard:
"""Load and validate a Server Card from a JSON file.

Raises:
OSError: If the file cannot be read.
json.JSONDecodeError: If the file is not valid JSON.
pydantic.ValidationError: If the document is not a valid Server Card.
"""
text = Path(path).read_text(encoding="utf-8")
return ServerCard.model_validate(json.loads(text))
124 changes: 124 additions & 0 deletions src/mcp/server/experimental/server_card.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Generate and serve MCP Server Cards (SEP-2127).

WARNING: These APIs are experimental and may change without notice.

A server author builds a card from the server's identity and either serves it
from the conventional ``.well-known`` path or hands it to their own Starlette
app::

from mcp.server.experimental.server_card import build_server_card, mount_server_card
from mcp.shared.experimental.server_card import Remote

card = build_server_card(
server,
name="io.modelcontextprotocol.examples/dice-roller",
remotes=[Remote(type="streamable-http", url="https://dice.example.com/mcp")],
)

app = server.streamable_http_app()
mount_server_card(app, card) # GET /.well-known/mcp/server-card

To write a card to a file instead, serialize it with
``card.model_dump_json(by_alias=True, exclude_none=True)``.
"""

from __future__ import annotations

from typing import Any, Protocol

from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route

from mcp.shared.experimental.server_card.types import (
WELL_KNOWN_PATH,
Icon,
Remote,
Repository,
ServerCard,
)

__all__ = ["build_server_card", "server_card_route", "mount_server_card"]


class _ServerIdentity(Protocol):
"""The identity attributes shared by the low-level ``Server`` and ``MCPServer``."""

name: str
version: str | None
title: str | None
description: str | None
website_url: str | None
icons: list[Icon] | None


def build_server_card(
server: _ServerIdentity,
*,
name: str,
remotes: list[Remote] | None = None,
repository: Repository | None = None,
meta: dict[str, Any] | None = None,
) -> ServerCard:
"""Build a Server Card from a running server's identity metadata.

``name`` is the card's reverse-DNS ``namespace/name`` identifier, passed
explicitly because a server's display ``name`` is free-form. The version,
title, description, website and icons are taken from ``server``.

Args:
server: A low-level ``Server`` or high-level ``MCPServer`` (anything
exposing the standard identity attributes).
name: Reverse-DNS server name, e.g. ``"io.modelcontextprotocol/everything"``.
remotes: Remote endpoints to advertise.
repository: Optional source repository metadata.
meta: Optional ``_meta`` extension metadata.

Returns:
A validated :class:`ServerCard`.

Raises:
ValueError: If ``server`` has no ``version`` or ``description`` set; both
are required on a card.
pydantic.ValidationError: If the resulting card is invalid (e.g. ``name``
is not reverse-DNS).
"""
if server.version is None:
raise ValueError("server.version must be set to build a Server Card")
if not server.description:
raise ValueError("server.description must be set to build a Server Card")
return ServerCard(
name=name,
version=server.version,
description=server.description,
title=server.title,
website_url=server.website_url,
icons=server.icons,
remotes=remotes,
repository=repository,
_meta=meta,
)


def server_card_route(card: ServerCard, *, path: str = WELL_KNOWN_PATH) -> Route:
"""Build a Starlette GET route that serves ``card`` as JSON at ``path``.

Add it to a new app — ``Starlette(routes=[server_card_route(card)])`` — or an
existing one via :func:`mount_server_card`. The payload is serialized once;
a card is static metadata.
"""
payload = card.model_dump(mode="json", by_alias=True, exclude_none=True)

async def endpoint(_request: Request) -> JSONResponse:
return JSONResponse(payload, media_type="application/json")

return Route(path, endpoint=endpoint, methods=["GET"], name="mcp_server_card")


def mount_server_card(app: Starlette, card: ServerCard, *, path: str = WELL_KNOWN_PATH) -> None:
"""Attach a Server Card route to an existing Starlette application.

The route is unauthenticated, which is what pre-connection discovery wants.
"""
app.router.routes.append(server_card_route(card, path=path))
55 changes: 55 additions & 0 deletions src/mcp/shared/experimental/server_card/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""MCP Server Cards (SEP-2127) — shared types.

WARNING: These APIs are experimental and may change without notice.

A Server Card is a static metadata document describing a remote MCP server,
suitable for pre-connection discovery. See
``mcp.shared.experimental.server_card.types`` for the model definitions.

* Servers generate and serve a card with ``mcp.server.experimental.server_card``.
* Clients ingest one with ``mcp.client.experimental.server_card``.
"""

from mcp.shared.experimental.server_card.types import (
SERVER_CARD_SCHEMA_URL,
SERVER_SCHEMA_URL,
WELL_KNOWN_PATH,
Argument,
Icon,
Input,
InputWithVariables,
KeyValueInput,
NamedArgument,
Package,
PackageTransport,
PositionalArgument,
Remote,
Repository,
Server,
ServerCard,
SsePackageTransport,
StdioTransport,
StreamableHttpPackageTransport,
)

__all__ = [
"SERVER_CARD_SCHEMA_URL",
"SERVER_SCHEMA_URL",
"WELL_KNOWN_PATH",
"Argument",
"Icon",
"Input",
"InputWithVariables",
"KeyValueInput",
"NamedArgument",
"Package",
"PackageTransport",
"PositionalArgument",
"Remote",
"Repository",
"Server",
"ServerCard",
"SsePackageTransport",
"StdioTransport",
"StreamableHttpPackageTransport",
]
Loading
Loading