chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:10:27 +08:00
commit 49b9bb6724
992 changed files with 161690 additions and 0 deletions
View File
+170
View File
@@ -0,0 +1,170 @@
"""Discovery + parametrization for the example-stories matrix.
Reads ``examples/stories/manifest.toml`` and expands each story across
(server_variant × transport × era). The story modules are imported as
real packages (the ``mcp-example-stories`` workspace member installs ``stories``
editable), so pyright sees them and a signature change red-lines every story.
The HTTP-ASGI leg reuses the interaction suite's in-process bridge directly
from ``tests.interaction.transports._bridge`` (both live under ``tests/``); the
move to ``stories._shared.bridge`` is a later batch.
"""
from __future__ import annotations
import importlib
import sys
from collections.abc import AsyncIterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import httpx
import pytest
import stories
from mcp_types.version import LATEST_MODERN_VERSION
from starlette.applications import Starlette
from stories._harness import AuthBuilder, TargetFactory
from stories._hosting import asgi_from
from mcp.client.streamable_http import streamable_http_client
from tests.interaction.transports._bridge import StreamingASGITransport
if sys.version_info >= (3, 11): # pragma: lax no cover
import tomllib
else: # pragma: lax no cover
import tomli as tomllib
STORIES_DIR = Path(stories.__file__).parent
BASE_URL = "http://127.0.0.1:8000"
MANIFEST = tomllib.loads((STORIES_DIR / "manifest.toml").read_text())
DEFAULTS: dict[str, Any] = MANIFEST["defaults"]
STORIES: dict[str, dict[str, Any]] = MANIFEST["story"]
_ERA_TO_MODE = {"modern": LATEST_MODERN_VERSION, "legacy": "legacy", "in-body": "auto"}
"""``Client`` rejects handshake-era version strings, so ``legacy`` resolves to
``mode='legacy'`` rather than ``LATEST_HANDSHAKE_VERSION``. ``in-body`` legs pin
their connection modes inside ``main`` themselves, so they get ``"auto"`` — the
``Client`` default; the era axis still passes every ``mode=`` explicitly."""
def story_cfg(name: str) -> dict[str, Any]:
return DEFAULTS | STORIES.get(name, {})
def _expand_era(era: str) -> tuple[str, ...]:
if era == "dual":
return ("modern", "legacy")
if era == "dual-in-body":
return ("in-body",)
return (era,)
@dataclass(frozen=True)
class Leg:
story: str
server_variant: str
transport: str
era: str
@property
def id(self) -> str:
return "-".join((self.story, self.server_variant, self.transport, self.era))
@property
def mode(self) -> str:
"""The explicit ``mode=`` this leg passes to the story's ``main``."""
return _ERA_TO_MODE[self.era]
def _legs() -> list[tuple[Leg, dict[str, Any]]]:
out: list[tuple[Leg, dict[str, Any]]] = []
for name in STORIES:
cfg = story_cfg(name)
variants = ["server"] + (["server_lowlevel"] if cfg["lowlevel"] else [])
out.extend(
(Leg(name, variant, transport, era), cfg)
for variant in variants
for transport in cfg["transports"]
for era in _expand_era(cfg["era"])
)
return out
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
if "leg" not in metafunc.fixturenames:
return
params: list[Any] = []
for leg, cfg in _legs():
marks: list[pytest.MarkDecorator] = []
if f"{leg.transport}:{leg.era}" in cfg["xfail"]:
marks.append(pytest.mark.xfail(strict=True, reason="manifest xfail")) # pragma: lax no cover
params.append(pytest.param(leg, marks=marks, id=leg.id))
metafunc.parametrize("leg", params)
@pytest.fixture
def cfg(leg: Leg) -> dict[str, Any]:
return story_cfg(leg.story)
@pytest.fixture
def server_module(leg: Leg) -> Any:
return importlib.import_module(f"stories.{leg.story}.{leg.server_variant}")
@pytest.fixture
def client_module(leg: Leg) -> Any:
return importlib.import_module(f"stories.{leg.story}.client")
@dataclass
class Hosted:
"""One server/app instance hosted for the leg's whole duration.
``targets`` yields a fresh connection target against that single instance on
every call, so state observed by one connection is visible to the next.
``http`` is the shared raw ``httpx.AsyncClient`` bound to the same ASGI app,
or ``None`` on the in-memory leg.
"""
targets: TargetFactory
http: httpx.AsyncClient | None
@pytest.fixture
async def hosted(
leg: Leg, cfg: dict[str, Any], server_module: Any, client_module: Any, monkeypatch: pytest.MonkeyPatch
) -> AsyncIterator[Hosted]:
"""Build the leg's server/app once and keep it running for the test.
The story's ``main`` owns the ``Client(target, mode=...)`` construction; this
fixture only decides what ``target`` is. Auth stories thread an ``httpx.Auth``
onto the bridge client via a module-level ``build_auth(http)`` export.
"""
for key, value in cfg["env"].items():
monkeypatch.setenv(key, value)
path = cfg["mcp_path"]
if leg.transport == "in-memory":
server = server_module.build_server()
yield Hosted(lambda: server, None)
return
# http-asgi: one Starlette app per leg. ``server_export="app"`` stories hand us the
# app directly; ``"factory"`` stories are wrapped via ``asgi_from``. Either way the
# app's own lifespan is what brings the session manager up, and the in-process
# bridge never fires ASGI lifespan events itself, so enter it explicitly.
if cfg["server_export"] == "app":
app: Starlette = server_module.build_app()
else:
app = asgi_from(server_module.build_server(), path=path)
build_auth: AuthBuilder | None = getattr(client_module, "build_auth", None)
async with (
app.router.lifespan_context(app),
httpx.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as http_client,
):
if build_auth is not None:
http_client.auth = build_auth(http_client)
yield Hosted(lambda: streamable_http_client(f"{BASE_URL}{path}", http_client=http_client), http_client)
+74
View File
@@ -0,0 +1,74 @@
"""Run every story's ``main`` over the in-process (transport × era × variant) matrix."""
from __future__ import annotations
import importlib
import inspect
from typing import Any
import anyio
import pytest
from tests.examples.conftest import MANIFEST, STORIES, STORIES_DIR, Hosted, Leg, story_cfg
pytestmark = pytest.mark.anyio
async def test_story(leg: Leg, cfg: dict[str, Any], hosted: Hosted, client_module: Any) -> None:
kwargs: dict[str, Any] = {"mode": leg.mode}
if cfg["needs_http"]:
kwargs["http"] = hosted.http
with anyio.fail_after(cfg["timeout_s"]):
if cfg["multi_connection"]:
await client_module.main(hosted.targets, **kwargs)
else:
await client_module.main(hosted.targets(), **kwargs)
def test_manifest_matches_filesystem() -> None:
"""Manifest [story.*] / [deferred] keys and on-disk story directories agree exactly."""
dirs = {d.name for d in STORIES_DIR.iterdir() if d.is_dir() and not d.name.startswith(("_", "."))}
runnable = {d for d in dirs if (STORIES_DIR / d / "client.py").exists()}
in_manifest = set(STORIES)
assert runnable == in_manifest, {"only_on_disk": runnable - in_manifest, "only_in_manifest": in_manifest - runnable}
# README-only stub dirs must be exactly the [deferred] table.
deferred_manifest = set(MANIFEST.get("deferred", {}))
assert dirs - runnable == deferred_manifest, {
"stub_dirs_missing_from_manifest": (dirs - runnable) - deferred_manifest,
"deferred_entries_missing_dir": deferred_manifest - (dirs - runnable),
}
assert runnable.isdisjoint(deferred_manifest), "deferred stories must not have a client.py"
_ERAS = {"dual", "modern", "legacy", "dual-in-body"}
_TRANSPORTS = {"in-memory", "http-asgi"}
_SERVER_EXPORTS = {"factory", "app"}
def test_manifest_schema_valid() -> None:
"""Declared manifest values are mutually consistent with the story files."""
for name in STORIES:
cfg = story_cfg(name)
assert "-" not in name, f"{name!r}: story directories must be underscored"
assert cfg["era"] in _ERAS, f"{name!r}: era={cfg['era']!r} not in {_ERAS}"
assert cfg["server_export"] in _SERVER_EXPORTS, f"{name!r}: server_export={cfg['server_export']!r}"
assert set(cfg["transports"]) <= _TRANSPORTS, f"{name!r}: transports={cfg['transports']!r}"
assert (STORIES_DIR / name / "__init__.py").exists(), f"{name!r}: missing __init__.py"
if cfg["server_export"] == "factory":
assert (STORIES_DIR / name / "server.py").exists(), f"{name!r}: missing server.py"
else:
assert "in-memory" not in cfg["transports"], f"{name!r}: server_export='app' cannot run in-memory"
if cfg["needs_http"]:
assert cfg["transports"] == ["http-asgi"], f"{name!r}: needs_http requires transports=['http-asgi']"
ll = STORIES_DIR / name / "server_lowlevel.py"
assert cfg["lowlevel"] == ll.exists(), f"{name!r}: lowlevel={cfg['lowlevel']} vs server_lowlevel.py on disk"
@pytest.mark.parametrize("name", sorted(STORIES))
def test_main_signature_matches_manifest(name: str) -> None:
"""``main``'s first parameter is ``target``/``targets`` per ``multi_connection``; ``http`` iff ``needs_http``."""
cfg = story_cfg(name)
params = list(inspect.signature(importlib.import_module(f"stories.{name}.client").main).parameters)
first = "targets" if cfg["multi_connection"] else "target"
assert params[0] == first, f"{name}: first param is {params[0]!r}, expected {first!r}"
assert ("http" in params) == cfg["needs_http"], f"{name}: 'http' param vs needs_http={cfg['needs_http']}"
+57
View File
@@ -0,0 +1,57 @@
"""Subprocess smoke for the story ``__main__`` paths.
The in-process matrix in ``test_stories.py`` never executes a story's
``if __name__ == "__main__"`` block, so ``run_client`` / ``run_server_from_args`` /
``run_app_from_args`` and the real stdio + uvicorn entries are unverified by
construction. This file proves that plumbing by running the literal commands the
story READMEs print: stdio (``run_client`` spawns the server over stdio) and bare
``--http`` (``run_client`` self-hosts the server on a real uvicorn socket on a
port it owns, then terminates it).
lax no cover: gated on ``MCP_EXAMPLES_SMOKE=1``, which CI sets on exactly one
matrix cell (ubuntu / 3.12 / locked — see ``shared.yml``). Every other cell
skips at collection, so the test body is uncovered there and the per-job 100%
gate would otherwise fail.
"""
from __future__ import annotations
import os
import sys
from pathlib import Path
import anyio
import pytest
pytestmark = [
pytest.mark.anyio,
pytest.mark.skipif(
os.environ.get("MCP_EXAMPLES_SMOKE") != "1",
reason="subprocess smoke runs on one CI cell only; set MCP_EXAMPLES_SMOKE=1",
),
]
_REPO_ROOT = Path(__file__).parents[2]
# httpx in the spawned client honours these and tries to mount a SOCKS transport even for
# 127.0.0.1; strip them so the smoke run is hermetic regardless of the caller's shell.
_PROXY_VARS = {v for base in ("all_proxy", "http_proxy", "https_proxy", "ftp_proxy") for v in (base, base.upper())}
_ENV = {k: v for k, v in os.environ.items() if k not in _PROXY_VARS}
@pytest.mark.parametrize(
"argv",
[
("stories.tools.client",),
("stories.tools.client", "--http"),
("stories.bearer_auth.client", "--http"),
],
ids=["tools-stdio", "tools-http", "bearer_auth-http"],
)
async def test_story_main_runs_end_to_end(argv: tuple[str, ...]) -> None: # pragma: lax no cover
"""``python -m <story>.client [--http]`` (the README command) exits 0 over a real subprocess."""
with anyio.fail_after(60):
async with await anyio.open_process(
[sys.executable, "-m", *argv], cwd=_REPO_ROOT, env=_ENV, stdout=None, stderr=None
) as proc:
await proc.wait()
assert proc.returncode == 0
+122
View File
@@ -0,0 +1,122 @@
"""AST shape-check: stories keep the SDK construction visible and the harness contained.
The python analogue of typescript-sdk's eslint import-allowlist over its examples,
strictly stronger: it also asserts each ``main`` constructs ``Client(...)`` itself —
the regression the harness inversion exists to prevent.
"""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
from tests.examples.conftest import STORIES, STORIES_DIR, story_cfg
_HARNESS_ALLOWLIST = frozenset({"run_client", "target_from_args", "Target", "TargetFactory"})
"""The only ``stories._harness`` names a ``client.py`` may use. ``AuthBuilder`` is
additionally allowed in a ``client.py`` that defines ``build_auth`` (the auth seam
``run_client`` and the conftest both look up by name)."""
_MCPSERVER_TIER = ("mcp.server.mcpserver", "mcp.server.MCPServer")
"""Both spellings of the high-level tier: the ``mcpserver`` module and its ``mcp.server`` re-export."""
_LOWLEVEL_STORIES = [name for name in sorted(STORIES) if story_cfg(name)["lowlevel"]]
def _parse(path: Path) -> ast.Module:
"""Parse ``path`` into an AST module."""
return ast.parse(path.read_text(), filename=str(path))
def _resolve(node: ast.ImportFrom, package: str) -> str:
"""The absolute module path ``node`` imports from, resolving a relative import against ``package``."""
parents = package.split(".")[: -(node.level - 1) or None] if node.level else []
return ".".join([*parents, *([node.module] if node.module else [])])
def _module_paths(tree: ast.Module, package: str) -> set[str]:
"""Every dotted module path the file (a module in ``package``) references — imports, with relative
ones resolved to absolute, plus attribute chains rooted at an import-bound name (``import mcp.shared``
+ ``mcp.shared._memory.f()``), so a reach-in is caught however it is spelled."""
paths: set[str] = set()
bound: dict[str, str] = {}
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
paths.add(alias.name)
local = alias.asname or alias.name.partition(".")[0]
bound[local] = alias.name if alias.asname else local
elif isinstance(node, ast.ImportFrom):
module = _resolve(node, package)
for alias in node.names:
paths.add(f"{module}.{alias.name}")
bound[alias.asname or alias.name] = f"{module}.{alias.name}"
for node in ast.walk(tree):
attrs: list[str] = []
expr: ast.AST = node
while isinstance(expr, ast.Attribute):
attrs.append(expr.attr)
expr = expr.value
if attrs and isinstance(expr, ast.Name) and expr.id in bound:
paths.add(".".join([bound[expr.id], *reversed(attrs)]))
return paths
def _is_private_mcp(path: str) -> bool:
"""True when ``path`` crosses a ``_``-private segment inside the ``mcp`` package."""
head, *rest = path.split(".")
return head == "mcp" and any(part.startswith("_") for part in rest)
def _is_story_module(path: str) -> bool:
"""True for ``stories.<story>...`` — a story package, not a ``stories._*`` scaffold."""
head, _, rest = path.partition(".")
return head == "stories" and bool(rest) and not rest.startswith("_")
@pytest.mark.parametrize("name", sorted(STORIES))
def test_main_constructs_client_inline(name: str) -> None:
"""``main``'s body contains a literal ``Client(...)`` call; the construction is never hidden in a helper."""
tree = _parse(STORIES_DIR / name / "client.py")
mains = [n for n in tree.body if isinstance(n, ast.AsyncFunctionDef) and n.name == "main"]
assert mains, f"{name}/client.py defines no top-level async `main`"
calls = {n.func.id for n in ast.walk(mains[0]) if isinstance(n, ast.Call) and isinstance(n.func, ast.Name)}
assert "Client" in calls, f"{name}/client.py: main() never calls Client(...) itself"
@pytest.mark.parametrize("name", sorted(STORIES))
def test_client_harness_imports_within_allowlist(name: str) -> None:
"""``client.py`` takes nothing from ``stories._harness`` beyond the allowlist, bounding the harness surface."""
tree = _parse(STORIES_DIR / name / "client.py")
defines_build_auth = any(isinstance(n, ast.FunctionDef) and n.name == "build_auth" for n in tree.body)
allowed = _HARNESS_ALLOWLIST | {"AuthBuilder"} if defines_build_auth else _HARNESS_ALLOWLIST
paths = _module_paths(tree, package=f"stories.{name}")
used = {p.removeprefix("stories._harness.").partition(".")[0] for p in paths if p.startswith("stories._harness.")}
assert used <= allowed, f"{name}/client.py uses {sorted(used - allowed)} from stories._harness"
@pytest.mark.parametrize("name", sorted(STORIES))
def test_story_files_import_no_private_mcp_module(name: str) -> None:
"""No file in a story directory references a ``_``-private ``mcp.*`` module."""
for path in sorted((STORIES_DIR / name).glob("*.py")):
private = sorted(p for p in _module_paths(_parse(path), package=f"stories.{name}") if _is_private_mcp(p))
assert not private, f"{path.relative_to(STORIES_DIR)} reaches into private mcp module(s): {private}"
@pytest.mark.parametrize("name", _LOWLEVEL_STORIES)
def test_server_lowlevel_imports_no_mcpserver_tier(name: str) -> None:
"""``server_lowlevel.py`` stays on the lowlevel tier; it never references ``MCPServer`` or its module."""
paths = _module_paths(_parse(STORIES_DIR / name / "server_lowlevel.py"), package=f"stories.{name}")
high = sorted(p for p in paths if any(f"{p}.".startswith(f"{tier}.") for tier in _MCPSERVER_TIER))
assert not high, f"{name}/server_lowlevel.py references the MCPServer tier: {high}"
@pytest.mark.parametrize("scaffold", ["_harness.py", "_hosting.py"])
def test_scaffold_imports_no_story_module(scaffold: str) -> None:
"""The dependency is one-way: ``_harness.py`` / ``_hosting.py`` import no ``stories.<story>`` module."""
story_refs = sorted(
p for p in _module_paths(_parse(STORIES_DIR / scaffold), package="stories") if _is_story_module(p)
)
assert not story_refs, f"{scaffold} imports a story module: {story_refs}"