25
25
ActionType ,
26
26
ActionWait ,
27
27
)
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
29
30
from openai .types .responses .response_reasoning_item import ResponseReasoningItem
30
31
31
32
from .agent import Agent , ToolsToFinalOutputResult
38
39
HandoffCallItem ,
39
40
HandoffOutputItem ,
40
41
ItemHelpers ,
42
+ MCPApprovalRequestItem ,
43
+ MCPApprovalResponseItem ,
44
+ MCPListToolsItem ,
41
45
MessageOutputItem ,
42
46
ModelResponse ,
43
47
ReasoningItem ,
52
56
from .models .interface import ModelTracing
53
57
from .run_context import RunContextWrapper , TContext
54
58
from .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
+ )
56
67
from .tracing import (
57
68
SpanError ,
58
69
Trace ,
@@ -112,22 +123,30 @@ class ToolRunComputerAction:
112
123
computer_tool : ComputerTool
113
124
114
125
126
+ @dataclass
127
+ class ToolRunMCPApprovalRequest :
128
+ request_item : McpApprovalRequest
129
+ mcp_tool : HostedMCPTool
130
+
131
+
115
132
@dataclass
116
133
class ProcessedResponse :
117
134
new_items : list [RunItem ]
118
135
handoffs : list [ToolRunHandoff ]
119
136
functions : list [ToolRunFunction ]
120
137
computer_actions : list [ToolRunComputerAction ]
121
138
tools_used : list [str ] # Names of all tools used, including hosted tools
139
+ mcp_approval_requests : list [ToolRunMCPApprovalRequest ] # Only requests with callbacks
122
140
123
- def has_tools_to_run (self ) -> bool :
141
+ def has_tools_or_approvals_to_run (self ) -> bool :
124
142
# Handoffs, functions and computer actions need local processing
125
143
# Hosted tools have already run, so there's nothing to do.
126
144
return any (
127
145
[
128
146
self .handoffs ,
129
147
self .functions ,
130
148
self .computer_actions ,
149
+ self .mcp_approval_requests ,
131
150
]
132
151
)
133
152
@@ -226,7 +245,16 @@ async def execute_tools_and_side_effects(
226
245
new_step_items .extend ([result .run_item for result in function_results ])
227
246
new_step_items .extend (computer_results )
228
247
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
230
258
if run_handoffs := processed_response .handoffs :
231
259
return await cls .execute_handoffs (
232
260
agent = agent ,
@@ -240,7 +268,7 @@ async def execute_tools_and_side_effects(
240
268
run_config = run_config ,
241
269
)
242
270
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
244
272
check_tool_use = await cls ._check_for_final_output_from_tools (
245
273
agent = agent ,
246
274
tool_results = function_results ,
@@ -295,7 +323,7 @@ async def execute_tools_and_side_effects(
295
323
)
296
324
elif (
297
325
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 ():
299
327
return await cls .execute_final_output (
300
328
agent = agent ,
301
329
original_input = original_input ,
@@ -343,10 +371,16 @@ def process_model_response(
343
371
run_handoffs = []
344
372
functions = []
345
373
computer_actions = []
374
+ mcp_approval_requests = []
346
375
tools_used : list [str ] = []
347
376
handoff_map = {handoff .tool_name : handoff for handoff in handoffs }
348
377
function_map = {tool .name : tool for tool in all_tools if isinstance (tool , FunctionTool )}
349
378
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
+ }
350
384
351
385
for output in response .output :
352
386
if isinstance (output , ResponseOutputMessage ):
@@ -375,6 +409,34 @@ def process_model_response(
375
409
computer_actions .append (
376
410
ToolRunComputerAction (tool_call = output , computer_tool = computer_tool )
377
411
)
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 )
378
440
elif not isinstance (output , ResponseFunctionToolCall ):
379
441
logger .warning (f"Unexpected output type, ignoring: { type (output )} " )
380
442
continue
@@ -417,6 +479,7 @@ def process_model_response(
417
479
functions = functions ,
418
480
computer_actions = computer_actions ,
419
481
tools_used = tools_used ,
482
+ mcp_approval_requests = mcp_approval_requests ,
420
483
)
421
484
422
485
@classmethod
@@ -643,6 +706,40 @@ async def execute_handoffs(
643
706
next_step = NextStepHandoff (new_agent ),
644
707
)
645
708
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
+
646
743
@classmethod
647
744
async def execute_final_output (
648
745
cls ,
@@ -727,6 +824,11 @@ def stream_step_result_to_queue(
727
824
event = RunItemStreamEvent (item = item , name = "tool_output" )
728
825
elif isinstance (item , ReasoningItem ):
729
826
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
+
730
832
else :
731
833
logger .warning (f"Unexpected item type: { type (item )} " )
732
834
event = None
0 commit comments