99 FunctionTool ,
1010 ModelBehaviorError ,
1111 RunContextWrapper ,
12+ RunHooks ,
1213 Runner ,
14+ Tool ,
1315 ToolGuardrailFunctionOutput ,
1416 ToolInputGuardrailData ,
1517 ToolOutputGuardrailData ,
2325
2426from ..test_responses import get_function_tool_call , get_text_message
2527from .helpers import FakeMCPServer
28+ from .model_compat import Tool as MCPTool
2629
2730
2831def _model_tool_outputs (model : ScriptedModel ) -> list [Any ]:
@@ -36,6 +39,68 @@ def _model_tool_outputs(model: ScriptedModel) -> list[Any]:
3639 return values
3740
3841
42+ @pytest .mark .asyncio
43+ @pytest .mark .parametrize ("streaming" , [False , True ])
44+ async def test_runner_hooks_can_read_mcp_annotations (streaming : bool ):
45+ hints = {
46+ "readOnlyHint" : False ,
47+ "destructiveHint" : True ,
48+ "idempotentHint" : False ,
49+ "openWorldHint" : False ,
50+ }
51+ server = FakeMCPServer (
52+ tools = [
53+ MCPTool (name = "write_record" , inputSchema = {}, annotations = hints ),
54+ MCPTool (name = "unclassified" , inputSchema = {}),
55+ ]
56+ )
57+ observed : list [tuple [str , str , dict [str , Any ] | None ]] = []
58+
59+ class Hooks (RunHooks ):
60+ def record (self , event : str , tool : Tool ) -> None :
61+ assert isinstance (tool , FunctionTool )
62+ annotations = tool .mcp_annotations
63+ observed .append (
64+ (
65+ event ,
66+ tool .name ,
67+ annotations .model_dump (by_alias = True , exclude_none = True )
68+ if annotations is not None
69+ else None ,
70+ )
71+ )
72+
73+ async def on_tool_start (self , context , agent , tool ):
74+ self .record ("start" , tool )
75+
76+ async def on_tool_end (self , context , agent , tool , result ):
77+ self .record ("end" , tool )
78+
79+ model = ScriptedModel (
80+ [
81+ [get_function_tool_call ("write_record" , "{}" )],
82+ [get_function_tool_call ("unclassified" , "{}" , call_id = "unclassified_call" )],
83+ [get_text_message ("done" )],
84+ ]
85+ )
86+ agent = Agent (name = "test" , model = model , mcp_servers = [server ])
87+ if streaming :
88+ result = Runner .run_streamed (agent , input = "user_message" , hooks = Hooks ())
89+ async for _ in result .stream_events ():
90+ pass
91+ else :
92+ result = await Runner .run (agent , input = "user_message" , hooks = Hooks ())
93+
94+ assert result .final_output == "done"
95+ assert server .tool_calls == ["write_record" , "unclassified" ]
96+ assert observed == [
97+ ("start" , "write_record" , hints ),
98+ ("end" , "write_record" , hints ),
99+ ("start" , "unclassified" , None ),
100+ ("end" , "unclassified" , None ),
101+ ]
102+
103+
39104@pytest .mark .asyncio
40105@pytest .mark .parametrize ("streaming" , [False , True ])
41106async def test_runner_calls_mcp_tool (streaming : bool ):
0 commit comments