44import json
55import weakref
66from collections .abc import Sequence
7- from typing import Any , TypeVar
7+ from typing import Any , TypeVar , cast
88
99import pytest
1010from mcp import Tool as MCPTool
11+ from openai .types .responses .response_output_item import McpCall , McpListTools , McpListToolsTool
1112from pydantic import BaseModel
1213
1314from agents import (
1415 Agent ,
16+ HostedMCPTool ,
1517 ModelResponse ,
1618 RunConfig ,
1719 RunContextWrapper ,
2527 Usage ,
2628 function_tool ,
2729)
30+ from agents .items import MCPListToolsItem , ToolApprovalItem
2831from agents .mcp import MCPUtil
2932from agents .run_internal import run_loop
33+ from agents .run_internal .agent_bindings import bind_public_agent
3034from agents .run_internal .run_loop import get_output_schema
3135from agents .run_internal .tool_execution import execute_function_tool_calls
3236from tests .fake_model import FakeModel
@@ -48,6 +52,22 @@ class StructuredOutputPayload(BaseModel):
4852 status : str
4953
5054
55+ def _make_hosted_mcp_list_tools (server_label : str , tool_name : str ) -> McpListTools :
56+ return McpListTools (
57+ id = f"list_{ server_label } " ,
58+ server_label = server_label ,
59+ tools = [
60+ McpListToolsTool (
61+ name = tool_name ,
62+ input_schema = {},
63+ description = "Search the docs." ,
64+ annotations = {"title" : "Search Docs" },
65+ )
66+ ],
67+ type = "mcp_list_tools" ,
68+ )
69+
70+
5171@pytest .mark .asyncio
5272async def test_runner_attaches_function_tool_origin_to_call_and_output_items () -> None :
5373 model = FakeModel ()
@@ -184,6 +204,104 @@ async def test_streamed_tool_call_item_includes_local_mcp_origin() -> None:
184204 )
185205
186206
207+ def test_process_model_response_attaches_hosted_mcp_tool_origin () -> None :
208+ agent = Agent (name = "hosted-mcp" )
209+ hosted_tool = HostedMCPTool (
210+ tool_config = cast (
211+ Any ,
212+ {
213+ "type" : "mcp" ,
214+ "server_label" : "docs_server" ,
215+ "server_url" : "https://example.com/mcp" ,
216+ },
217+ )
218+ )
219+ existing_items = [
220+ MCPListToolsItem (
221+ agent = agent ,
222+ raw_item = _make_hosted_mcp_list_tools ("docs_server" , "search_docs" ),
223+ )
224+ ]
225+ response = ModelResponse (
226+ output = [
227+ McpCall (
228+ id = "mcp_call_1" ,
229+ arguments = "{}" ,
230+ name = "search_docs" ,
231+ server_label = "docs_server" ,
232+ type = "mcp_call" ,
233+ status = "completed" ,
234+ )
235+ ],
236+ usage = Usage (),
237+ response_id = "resp_hosted_mcp" ,
238+ )
239+
240+ processed = run_loop .process_model_response (
241+ agent = agent ,
242+ all_tools = [hosted_tool ],
243+ response = response ,
244+ output_schema = None ,
245+ handoffs = [],
246+ existing_items = existing_items ,
247+ )
248+
249+ tool_call_item = _first_item (processed .new_items , ToolCallItem )
250+ assert tool_call_item .tool_origin == ToolOrigin (
251+ type = ToolOriginType .MCP ,
252+ mcp_server_name = "docs_server" ,
253+ )
254+
255+
256+ @pytest .mark .asyncio
257+ async def test_streamed_tool_call_item_includes_hosted_mcp_origin () -> None :
258+ model = FakeModel ()
259+ hosted_tool = HostedMCPTool (
260+ tool_config = cast (
261+ Any ,
262+ {
263+ "type" : "mcp" ,
264+ "server_label" : "docs_server" ,
265+ "server_url" : "https://example.com/mcp" ,
266+ },
267+ )
268+ )
269+ agent = Agent (name = "stream-hosted-mcp" , model = model , tools = [hosted_tool ])
270+ model .add_multiple_turn_outputs (
271+ [
272+ [
273+ _make_hosted_mcp_list_tools ("docs_server" , "search_docs" ),
274+ McpCall (
275+ id = "mcp_call_stream_1" ,
276+ arguments = "{}" ,
277+ name = "search_docs" ,
278+ server_label = "docs_server" ,
279+ type = "mcp_call" ,
280+ status = "completed" ,
281+ ),
282+ ],
283+ [get_text_message ("done" )],
284+ ]
285+ )
286+
287+ result = Runner .run_streamed (agent , input = "hello" )
288+ seen_tool_item : ToolCallItem | None = None
289+ async for event in result .stream_events ():
290+ if (
291+ event .type == "run_item_stream_event"
292+ and isinstance (event .item , ToolCallItem )
293+ and isinstance (event .item .raw_item , McpCall )
294+ ):
295+ seen_tool_item = event .item
296+ break
297+
298+ assert seen_tool_item is not None
299+ assert seen_tool_item .tool_origin == ToolOrigin (
300+ type = ToolOriginType .MCP ,
301+ mcp_server_name = "docs_server" ,
302+ )
303+
304+
187305def test_local_mcp_tool_origin_does_not_retain_server_object () -> None :
188306 server = FakeMCPServer (server_name = "docs_server" )
189307 function_tool = MCPUtil .to_function_tool (
@@ -245,7 +363,7 @@ async def test_json_tool_call_does_not_emit_function_tool_origin() -> None:
245363 assert tool_call_item .tool_origin is None
246364
247365 function_results , _ , _ = await execute_function_tool_calls (
248- agent = agent ,
366+ bindings = bind_public_agent ( agent ) ,
249367 tool_runs = processed .functions ,
250368 hooks = RunHooks (),
251369 context_wrapper = context_wrapper ,
@@ -332,3 +450,52 @@ async def test_run_state_from_json_reads_legacy_1_5_without_tool_origin() -> Non
332450 restored_item = _first_item (restored ._generated_items , ToolCallItem )
333451 assert restored_item .description == "Legacy tool"
334452 assert restored_item .tool_origin is None
453+
454+
455+ @pytest .mark .asyncio
456+ async def test_run_state_roundtrip_preserves_tool_origin_on_approval_interruptions () -> None :
457+ agent = Agent (name = "approval-origin" )
458+ state : RunState [Any , Agent [Any ]] = make_run_state (agent )
459+ state ._generated_items .append (
460+ ToolApprovalItem (
461+ agent = agent ,
462+ raw_item = make_tool_call (call_id = "call_approval" , name = "approval_tool" ),
463+ tool_name = "approval_tool" ,
464+ tool_origin = ToolOrigin (type = ToolOriginType .FUNCTION ),
465+ )
466+ )
467+
468+ restored = await roundtrip_state (agent , state )
469+
470+ approval_item = _first_item (restored ._generated_items , ToolApprovalItem )
471+ assert approval_item .tool_origin == ToolOrigin (type = ToolOriginType .FUNCTION )
472+
473+
474+ @pytest .mark .asyncio
475+ async def test_run_state_from_json_reads_legacy_1_6_approval_without_tool_origin () -> None :
476+ agent = Agent (name = "approval-origin-legacy" )
477+ state : RunState [Any , Agent [Any ]] = make_run_state (agent )
478+ state ._generated_items .append (
479+ ToolApprovalItem (
480+ agent = agent ,
481+ raw_item = make_tool_call (call_id = "call_legacy_approval" , name = "approval_tool" ),
482+ tool_name = "approval_tool" ,
483+ tool_origin = ToolOrigin (type = ToolOriginType .FUNCTION ),
484+ )
485+ )
486+
487+ restored = await roundtrip_state (
488+ agent ,
489+ state ,
490+ mutate_json = lambda data : {
491+ ** data ,
492+ "$schemaVersion" : "1.6" ,
493+ "generated_items" : [
494+ {key : value for key , value in item .items () if key != "tool_origin" }
495+ for item in data ["generated_items" ]
496+ ],
497+ },
498+ )
499+
500+ approval_item = _first_item (restored ._generated_items , ToolApprovalItem )
501+ assert approval_item .tool_origin is None
0 commit comments