2525 ActionType ,
2626 ActionWait ,
2727)
28- from openai .types .responses .response_input_param import ComputerCallOutput
28+ from openai .types .responses .response_input_param import ComputerCallOutput , McpApprovalResponse
29+ from openai .types .responses .response_output_item import McpApprovalRequest , McpCall , McpListTools
2930from openai .types .responses .response_reasoning_item import ResponseReasoningItem
3031
3132from .agent import Agent , ToolsToFinalOutputResult
3839 HandoffCallItem ,
3940 HandoffOutputItem ,
4041 ItemHelpers ,
42+ MCPApprovalRequestItem ,
43+ MCPApprovalResponseItem ,
44+ MCPListToolsItem ,
4145 MessageOutputItem ,
4246 ModelResponse ,
4347 ReasoningItem ,
5256from .models .interface import ModelTracing
5357from .run_context import RunContextWrapper , TContext
5458from .stream_events import RunItemStreamEvent , StreamEvent
55- from .tool import ComputerTool , FunctionTool , FunctionToolResult , Tool
59+ from .tool import (
60+ ComputerTool ,
61+ FunctionTool ,
62+ FunctionToolResult ,
63+ HostedMCPTool ,
64+ MCPToolApprovalRequest ,
65+ Tool ,
66+ )
5667from .tracing import (
5768 SpanError ,
5869 Trace ,
@@ -112,22 +123,30 @@ class ToolRunComputerAction:
112123 computer_tool : ComputerTool
113124
114125
126+ @dataclass
127+ class ToolRunMCPApprovalRequest :
128+ request_item : McpApprovalRequest
129+ mcp_tool : HostedMCPTool
130+
131+
115132@dataclass
116133class ProcessedResponse :
117134 new_items : list [RunItem ]
118135 handoffs : list [ToolRunHandoff ]
119136 functions : list [ToolRunFunction ]
120137 computer_actions : list [ToolRunComputerAction ]
121138 tools_used : list [str ] # Names of all tools used, including hosted tools
139+ mcp_approval_requests : list [ToolRunMCPApprovalRequest ] # Only requests with callbacks
122140
123- def has_tools_to_run (self ) -> bool :
141+ def has_tools_or_approvals_to_run (self ) -> bool :
124142 # Handoffs, functions and computer actions need local processing
125143 # Hosted tools have already run, so there's nothing to do.
126144 return any (
127145 [
128146 self .handoffs ,
129147 self .functions ,
130148 self .computer_actions ,
149+ self .mcp_approval_requests ,
131150 ]
132151 )
133152
@@ -226,7 +245,16 @@ async def execute_tools_and_side_effects(
226245 new_step_items .extend ([result .run_item for result in function_results ])
227246 new_step_items .extend (computer_results )
228247
229- # Second, check if there are any handoffs
248+ # Next, run the MCP approval requests
249+ if processed_response .mcp_approval_requests :
250+ approval_results = await cls .execute_mcp_approval_requests (
251+ agent = agent ,
252+ approval_requests = processed_response .mcp_approval_requests ,
253+ context_wrapper = context_wrapper ,
254+ )
255+ new_step_items .extend (approval_results )
256+
257+ # Next, check if there are any handoffs
230258 if run_handoffs := processed_response .handoffs :
231259 return await cls .execute_handoffs (
232260 agent = agent ,
@@ -240,7 +268,7 @@ async def execute_tools_and_side_effects(
240268 run_config = run_config ,
241269 )
242270
243- # Third , we'll check if the tool use should result in a final output
271+ # Next , we'll check if the tool use should result in a final output
244272 check_tool_use = await cls ._check_for_final_output_from_tools (
245273 agent = agent ,
246274 tool_results = function_results ,
@@ -295,7 +323,7 @@ async def execute_tools_and_side_effects(
295323 )
296324 elif (
297325 not output_schema or output_schema .is_plain_text ()
298- ) and not processed_response .has_tools_to_run ():
326+ ) and not processed_response .has_tools_or_approvals_to_run ():
299327 return await cls .execute_final_output (
300328 agent = agent ,
301329 original_input = original_input ,
@@ -343,10 +371,16 @@ def process_model_response(
343371 run_handoffs = []
344372 functions = []
345373 computer_actions = []
374+ mcp_approval_requests = []
346375 tools_used : list [str ] = []
347376 handoff_map = {handoff .tool_name : handoff for handoff in handoffs }
348377 function_map = {tool .name : tool for tool in all_tools if isinstance (tool , FunctionTool )}
349378 computer_tool = next ((tool for tool in all_tools if isinstance (tool , ComputerTool )), None )
379+ hosted_mcp_server_map = {
380+ tool .tool_config ["server_label" ]: tool
381+ for tool in all_tools
382+ if isinstance (tool , HostedMCPTool )
383+ }
350384
351385 for output in response .output :
352386 if isinstance (output , ResponseOutputMessage ):
@@ -375,6 +409,34 @@ def process_model_response(
375409 computer_actions .append (
376410 ToolRunComputerAction (tool_call = output , computer_tool = computer_tool )
377411 )
412+ elif isinstance (output , McpApprovalRequest ):
413+ items .append (MCPApprovalRequestItem (raw_item = output , agent = agent ))
414+ if output .server_label not in hosted_mcp_server_map :
415+ _error_tracing .attach_error_to_current_span (
416+ SpanError (
417+ message = "MCP server label not found" ,
418+ data = {"server_label" : output .server_label },
419+ )
420+ )
421+ raise ModelBehaviorError (f"MCP server label { output .server_label } not found" )
422+ else :
423+ server = hosted_mcp_server_map [output .server_label ]
424+ if server .on_approval_request :
425+ mcp_approval_requests .append (
426+ ToolRunMCPApprovalRequest (
427+ request_item = output ,
428+ mcp_tool = server ,
429+ )
430+ )
431+ else :
432+ logger .warning (
433+ f"MCP server { output .server_label } has no on_approval_request hook"
434+ )
435+ elif isinstance (output , McpListTools ):
436+ items .append (MCPListToolsItem (raw_item = output , agent = agent ))
437+ elif isinstance (output , McpCall ):
438+ items .append (ToolCallItem (raw_item = output , agent = agent ))
439+ tools_used .append (output .name )
378440 elif not isinstance (output , ResponseFunctionToolCall ):
379441 logger .warning (f"Unexpected output type, ignoring: { type (output )} " )
380442 continue
@@ -417,6 +479,7 @@ def process_model_response(
417479 functions = functions ,
418480 computer_actions = computer_actions ,
419481 tools_used = tools_used ,
482+ mcp_approval_requests = mcp_approval_requests ,
420483 )
421484
422485 @classmethod
@@ -643,6 +706,40 @@ async def execute_handoffs(
643706 next_step = NextStepHandoff (new_agent ),
644707 )
645708
709+ @classmethod
710+ async def execute_mcp_approval_requests (
711+ cls ,
712+ * ,
713+ agent : Agent [TContext ],
714+ approval_requests : list [ToolRunMCPApprovalRequest ],
715+ context_wrapper : RunContextWrapper [TContext ],
716+ ) -> list [RunItem ]:
717+ async def run_single_approval (approval_request : ToolRunMCPApprovalRequest ) -> RunItem :
718+ callback = approval_request .mcp_tool .on_approval_request
719+ assert callback is not None , "Callback is required for MCP approval requests"
720+ maybe_awaitable_result = callback (
721+ MCPToolApprovalRequest (context_wrapper , approval_request .request_item )
722+ )
723+ if inspect .isawaitable (maybe_awaitable_result ):
724+ result = await maybe_awaitable_result
725+ else :
726+ result = maybe_awaitable_result
727+ reason = result .get ("reason" , None )
728+ raw_item : McpApprovalResponse = {
729+ "approval_request_id" : approval_request .request_item .id ,
730+ "approve" : result ["approve" ],
731+ "type" : "mcp_approval_response" ,
732+ }
733+ if not result ["approve" ] and reason :
734+ raw_item ["reason" ] = reason
735+ return MCPApprovalResponseItem (
736+ raw_item = raw_item ,
737+ agent = agent ,
738+ )
739+
740+ tasks = [run_single_approval (approval_request ) for approval_request in approval_requests ]
741+ return await asyncio .gather (* tasks )
742+
646743 @classmethod
647744 async def execute_final_output (
648745 cls ,
@@ -727,6 +824,11 @@ def stream_step_result_to_queue(
727824 event = RunItemStreamEvent (item = item , name = "tool_output" )
728825 elif isinstance (item , ReasoningItem ):
729826 event = RunItemStreamEvent (item = item , name = "reasoning_item_created" )
827+ elif isinstance (item , MCPApprovalRequestItem ):
828+ event = RunItemStreamEvent (item = item , name = "mcp_approval_requested" )
829+ elif isinstance (item , MCPListToolsItem ):
830+ event = RunItemStreamEvent (item = item , name = "mcp_list_tools" )
831+
730832 else :
731833 logger .warning (f"Unexpected item type: { type (item )} " )
732834 event = None
0 commit comments