66Contract:
77 - MCP_CONFORMANCE_SCENARIO env var -> scenario name
88 - MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for client-credentials scenarios)
9+ - MCP_CONFORMANCE_PROTOCOL_VERSION env var -> spec version the harness mock
10+ server is speaking (e.g. "2025-11-25", "2026-07-28"). Always set; when
11+ --spec-version is omitted the harness picks per-scenario (LATEST_SPEC_VERSION
12+ for active scenarios, DRAFT_PROTOCOL_VERSION for draft-only ones).
913 - Server URL as last CLI argument (sys.argv[1])
1014 - Must exit 0 within 30 seconds
1115
4044)
4145from mcp .client .context import ClientRequestContext
4246from mcp .client .streamable_http import streamable_http_client
43- from mcp .shared .auth import OAuthClientInformationFull , OAuthClientMetadata , OAuthToken
47+ from mcp .shared .auth import AuthorizationCodeResult , OAuthClientInformationFull , OAuthClientMetadata , OAuthToken
4448
4549# Set up logging to stderr (stdout is for conformance test output)
4650logging .basicConfig (
5054)
5155logger = logging .getLogger (__name__ )
5256
57+ #: Spec version the harness is running this scenario at (e.g. "2025-11-25",
58+ #: "2026-07-28"). The harness always sets this (when --spec-version is omitted
59+ #: it picks per-scenario: LATEST_SPEC_VERSION for active scenarios,
60+ #: DRAFT_PROTOCOL_VERSION for draft-only ones), so None means we were invoked
61+ #: outside the harness. Handlers that need to take the stateless 2026 path will
62+ #: branch on this once the SDK has one; today it is logged only.
63+ PROTOCOL_VERSION : str | None = os .environ .get ("MCP_CONFORMANCE_PROTOCOL_VERSION" )
64+
5365# Type for async scenario handler functions
5466ScenarioHandler = Callable [[str ], Coroutine [Any , None , None ]]
5567
@@ -109,6 +121,7 @@ class ConformanceOAuthCallbackHandler:
109121 def __init__ (self ) -> None :
110122 self ._auth_code : str | None = None
111123 self ._state : str | None = None
124+ self ._iss : str | None = None
112125
113126 async def handle_redirect (self , authorization_url : str ) -> None :
114127 """Fetch the authorization URL and extract the auth code from the redirect."""
@@ -130,6 +143,8 @@ async def handle_redirect(self, authorization_url: str) -> None:
130143 self ._auth_code = query_params ["code" ][0 ]
131144 state_values = query_params .get ("state" )
132145 self ._state = state_values [0 ] if state_values else None
146+ iss_values = query_params .get ("iss" )
147+ self ._iss = iss_values [0 ] if iss_values else None
133148 logger .debug (f"Got auth code from redirect: { self ._auth_code [:10 ]} ..." )
134149 return
135150 else :
@@ -139,15 +154,15 @@ async def handle_redirect(self, authorization_url: str) -> None:
139154 else :
140155 raise RuntimeError (f"Expected redirect response, got { response .status_code } from { authorization_url } " )
141156
142- async def handle_callback (self ) -> tuple [ str , str | None ] :
143- """Return the captured auth code and state ."""
157+ async def handle_callback (self ) -> AuthorizationCodeResult :
158+ """Return the captured auth code, state, and iss ."""
144159 if self ._auth_code is None :
145160 raise RuntimeError ("No authorization code available - was handle_redirect called?" )
146- auth_code = self ._auth_code
147- state = self ._state
161+ result = AuthorizationCodeResult (code = self ._auth_code , state = self ._state , iss = self ._iss )
148162 self ._auth_code = None
149163 self ._state = None
150- return auth_code , state
164+ self ._iss = None
165+ return result
151166
152167
153168# --- Scenario Handlers ---
@@ -164,6 +179,18 @@ async def run_initialize(server_url: str) -> None:
164179 logger .debug ("Listed tools successfully" )
165180
166181
182+ @register ("json-schema-ref-no-deref" )
183+ async def run_json_schema_ref_no_deref (server_url : str ) -> None :
184+ """Initialize and list tools; the scenario fails only if the client fetches a network $ref.
185+
186+ ClientSession never walks inputSchema or resolves $refs, so listing is enough (SEP-2106).
187+ """
188+ async with streamable_http_client (url = server_url ) as (read_stream , write_stream ):
189+ async with ClientSession (read_stream , write_stream ) as session :
190+ await session .initialize ()
191+ await session .list_tools ()
192+
193+
167194@register ("tools_call" )
168195async def run_tools_call (server_url : str ) -> None :
169196 """Connect, initialize, list tools, call add_numbers(a=5, b=3), close."""
@@ -347,6 +374,7 @@ def main() -> None:
347374
348375 server_url = sys .argv [1 ]
349376 scenario = os .environ .get ("MCP_CONFORMANCE_SCENARIO" )
377+ logger .debug (f"Conformance protocol version: { PROTOCOL_VERSION !r} " )
350378
351379 if scenario :
352380 logger .debug (f"Running explicit scenario '{ scenario } ' against { server_url } " )
0 commit comments