2020 json-schema-ref-no-deref - Connect, list tools (no $ref deref)
2121 request-metadata - Connect with all callbacks; client stamps _meta
2222 http-standard-headers - Connect, call a tool (Mcp-* headers checked)
23+ http-invalid-tool-headers - List tools, call every surfaced tool (x-mcp-header filter)
2324 elicitation-sep1034-client-defaults - Elicitation with default accept callback
25+ sep-2322-client-request-state - Drive the manual MRTR retry surface
2426 auth/client-credentials-jwt - Client credentials with private_key_jwt
2527 auth/client-credentials-basic - Client credentials with client_secret_basic
2628 auth/* - Authorization code flow (default for auth scenarios)
@@ -296,6 +298,43 @@ async def run_http_standard_headers(server_url: str) -> None:
296298 logger .debug (f"add_numbers result: { result } " )
297299
298300
301+ def _stub_required_args (input_schema : dict [str , Any ]) -> dict [str , Any ]:
302+ """Minimal arguments satisfying a tool inputSchema's required list."""
303+ by_type : dict [str , Any ] = {
304+ "string" : "x" ,
305+ "integer" : 0 ,
306+ "number" : 0 ,
307+ "boolean" : False ,
308+ "object" : {},
309+ "array" : [],
310+ "null" : None ,
311+ }
312+ properties = input_schema .get ("properties" , {})
313+ return {name : by_type .get (properties .get (name , {}).get ("type" ), "x" ) for name in input_schema .get ("required" , [])}
314+
315+
316+ @register ("http-invalid-tool-headers" )
317+ async def run_http_invalid_tool_headers (server_url : str ) -> None :
318+ """List tools, then call every tool the SDK surfaces (SEP-2243).
319+
320+ The harness mock advertises one valid tool plus several with malformed
321+ x-mcp-header annotations (empty, non-primitive type, duplicate, invalid
322+ chars). The scenario passes if valid_tool is called and the malformed
323+ ones are not -- so a conforming client filters them out of the list_tools
324+ result and the loop below never sees them. The scenario sets
325+ allowClientError, so a per-call failure is logged and skipped rather
326+ than aborting the whole run.
327+ """
328+ async with Client (server_url , mode = client_mode ()) as client :
329+ listed = await client .list_tools ()
330+ logger .debug (f"Surfaced tools: { [t .name for t in listed .tools ]} " )
331+ for tool in listed .tools :
332+ try :
333+ await client .call_tool (tool .name , _stub_required_args (tool .input_schema ))
334+ except Exception :
335+ logger .exception (f"call_tool({ tool .name !r} ) failed" )
336+
337+
299338@register ("elicitation-sep1034-client-defaults" )
300339async def run_elicitation_defaults (server_url : str ) -> None :
301340 """Connect with elicitation callback that applies schema defaults."""
@@ -305,6 +344,53 @@ async def run_elicitation_defaults(server_url: str) -> None:
305344 logger .debug (f"test_client_elicitation_defaults result: { result } " )
306345
307346
347+ @register ("sep-2322-client-request-state" )
348+ async def run_mrtr_client (server_url : str ) -> None :
349+ """Drive the manual MRTR retry surface against the SEP-2322 client mock.
350+
351+ The mock speaks the modern lifecycle (server/discover, no initialize) and
352+ inspects the wire params of each tools/call round, so this exercises the
353+ explicit allow_input_required=True path rather than an auto-loop: round 1
354+ receives an InputRequiredResult, the fixture fulfils the elicitation
355+ locally, then round 2 retries with input_responses + the echoed
356+ request_state. Passing request_state straight off the typed result -- a
357+ str when the server sent one, None when it didn't -- lets the
358+ serializer's exclude_none drop the key in the no-state case without a
359+ branch here. The unrelated call between rounds proves MRTR params don't
360+ leak across tools, and the no-result-type call must parse as a complete
361+ CallToolResult with no retry.
362+ """
363+ async with Client (server_url , mode = client_mode ()) as client :
364+ await client .list_tools ()
365+ confirm = {"confirm" : types .ElicitResult (action = "accept" , content = {"confirmed" : True })}
366+
367+ r1 = await client .call_tool ("test_mrtr_echo_state" , {}, allow_input_required = True )
368+ assert isinstance (r1 , types .InputRequiredResult )
369+
370+ await client .call_tool ("test_mrtr_unrelated" , {})
371+
372+ await client .call_tool (
373+ "test_mrtr_echo_state" ,
374+ {},
375+ input_responses = confirm ,
376+ request_state = r1 .request_state ,
377+ allow_input_required = True ,
378+ )
379+
380+ r2 = await client .call_tool ("test_mrtr_no_state" , {}, allow_input_required = True )
381+ assert isinstance (r2 , types .InputRequiredResult )
382+ await client .call_tool (
383+ "test_mrtr_no_state" ,
384+ {},
385+ input_responses = confirm ,
386+ request_state = r2 .request_state ,
387+ allow_input_required = True ,
388+ )
389+
390+ result = await client .call_tool ("test_mrtr_no_result_type" , {})
391+ assert isinstance (result , types .CallToolResult )
392+
393+
308394@register ("auth/client-credentials-jwt" )
309395async def run_client_credentials_jwt (server_url : str ) -> None :
310396 """Client credentials flow with private_key_jwt authentication."""
@@ -441,8 +527,7 @@ def main() -> None:
441527 asyncio .run (run_auth_code_client (server_url ))
442528 else :
443529 # Unhandled scenarios:
444- # - sep-2322-client-request-state (SEP-2322 / S6: MRTR client loop)
445- # - http-custom-headers, http-invalid-tool-headers (SEP-2243 / S8: Mcp-Param-* headers)
530+ # - http-custom-headers (SEP-2243 / S8: Mcp-Param-* emission)
446531 print (f"Unknown scenario: { scenario } " , file = sys .stderr )
447532 sys .exit (1 )
448533 else :
0 commit comments