3939
4040import pytest
4141
42- SPEC_REVISION = "2025-11-25"
42+ from mcp .shared .version import KNOWN_PROTOCOL_VERSIONS
43+
44+ SpecVersion = Literal ["2025-11-25" , "2026-07-28" ]
45+ """A protocol version the suite parametrizes over. Both values are typed even though only one is
46+ on the active axis (SPEC_VERSIONS) until the 2026-07-28 implementation lands."""
47+
48+ SPEC_VERSIONS : tuple [SpecVersion , ...] = ("2025-11-25" ,)
49+ """The active spec-version matrix axis, ordered oldest to newest. Every entry must be in KNOWN_PROTOCOL_VERSIONS."""
50+
51+ SPEC_REVISION = SPEC_VERSIONS [- 1 ]
4352SPEC_BASE_URL = f"https://modelcontextprotocol.io/specification/{ SPEC_REVISION } "
4453
4554Transport = Literal ["in-memory" , "stdio" , "streamable-http" , "sse" ]
4655
56+ CONNECTABLE_TRANSPORTS : tuple [Transport , ...] = ("in-memory" , "sse" , "streamable-http" )
57+ """Transports the connect fixture fans out over (the subset with a factory in conftest._FACTORIES)."""
58+
59+ TRANSPORT_SPEC_VERSIONS : dict [Transport , tuple [SpecVersion , ...]] = {
60+ "sse" : ("2025-11-25" ,),
61+ }
62+ """Transports that only serve a subset of SPEC_VERSIONS. Absent => serves all. Consulted by compute_cells()."""
63+
64+ ArmExclusionReason = Literal [
65+ "asserts-legacy-handshake" ,
66+ "method-not-in-modern-registry" ,
67+ "legacy-only-vocabulary" ,
68+ "modern-error-surface" ,
69+ "requires-session" ,
70+ "drives-transport-directly" ,
71+ "server-initiated-request" ,
72+ ]
73+ """Machine-readable reasons a requirement is excluded from a (transport, spec_version) matrix cell.
74+ The set doubles as a re-admission checklist: when a feature lands, grep for its reason to find the
75+ cells to re-admit. Values are kept byte-identical to the typescript-sdk's EntryExclusionReason."""
76+
4777_TestFn = TypeVar ("_TestFn" , bound = Callable [..., object ])
4878
4979_SOURCE_PATTERN = re .compile (r"https://modelcontextprotocol\.io/specification/.+|sdk|issue:#\d+" )
@@ -63,6 +93,38 @@ class Divergence:
6393 issue : str | None = None
6494
6595
96+ @dataclass (frozen = True , kw_only = True )
97+ class ArmExclusion :
98+ """Excludes a requirement from a (transport, spec_version) matrix cell, with a typed reason."""
99+
100+ reason : ArmExclusionReason
101+ transport : Transport | None = None
102+ spec_version : SpecVersion | None = None
103+ note : str | None = None
104+
105+ def __post_init__ (self ) -> None :
106+ if self .spec_version is not None and self .spec_version not in KNOWN_PROTOCOL_VERSIONS :
107+ raise ValueError (f"spec_version { self .spec_version !r} is not in KNOWN_PROTOCOL_VERSIONS" )
108+
109+
110+ @dataclass (frozen = True , kw_only = True )
111+ class KnownFailure :
112+ """A (transport, spec_version) cell where the requirement's test is expected to fail (strict xfail)."""
113+
114+ note : str
115+ transport : Transport | None = None
116+ spec_version : SpecVersion | None = None
117+ issue : str | None = None
118+
119+ def __post_init__ (self ) -> None :
120+ if not self .note .strip ():
121+ raise ValueError ("note must be non-empty" )
122+ if self .spec_version is not None and self .spec_version not in KNOWN_PROTOCOL_VERSIONS :
123+ raise ValueError (f"spec_version { self .spec_version !r} is not in KNOWN_PROTOCOL_VERSIONS" )
124+ if self .issue is not None and not re .fullmatch (r"#\d+|https://github\.com/\S+" , self .issue ):
125+ raise ValueError (f"issue must be '#<n>' or a GitHub URL, got { self .issue !r} " )
126+
127+
66128@dataclass (frozen = True , kw_only = True )
67129class Requirement :
68130 """A single testable behaviour and the provenance of why it must hold."""
@@ -73,10 +135,27 @@ class Requirement:
73135 divergence : Divergence | None = None
74136 deferred : str | None = None
75137 issue : str | None = None
138+ note : str | None = None
139+ added_in : SpecVersion | None = None
140+ removed_in : SpecVersion | None = None
141+ supersedes : tuple [str , ...] = ()
142+ superseded_by : str | None = None
143+ arm_exclusions : tuple [ArmExclusion , ...] = ()
144+ known_failures : tuple [KnownFailure , ...] = ()
76145
77146 def __post_init__ (self ) -> None :
78147 if not _SOURCE_PATTERN .fullmatch (self .source ):
79148 raise ValueError (f"source must be a specification URL, 'sdk', or 'issue:#n', got { self .source !r} " )
149+ if self .added_in is not None and self .added_in not in KNOWN_PROTOCOL_VERSIONS :
150+ raise ValueError (f"added_in { self .added_in !r} is not in KNOWN_PROTOCOL_VERSIONS" )
151+ if self .removed_in is not None and self .removed_in not in KNOWN_PROTOCOL_VERSIONS :
152+ raise ValueError (f"removed_in { self .removed_in !r} is not in KNOWN_PROTOCOL_VERSIONS" )
153+ if (
154+ self .added_in is not None
155+ and self .removed_in is not None
156+ and KNOWN_PROTOCOL_VERSIONS .index (self .added_in ) >= KNOWN_PROTOCOL_VERSIONS .index (self .removed_in )
157+ ):
158+ raise ValueError (f"added_in { self .added_in !r} must be earlier than removed_in { self .removed_in !r} " )
80159
81160
82161REQUIREMENTS : dict [str , Requirement ] = {
0 commit comments