Skip to content

Commit 9f8eb04

Browse files
committed
Hosted MCP support
1 parent ce2e2a4 commit 9f8eb04

File tree

9 files changed

+332
-11
lines changed

9 files changed

+332
-11
lines changed

examples/hosted_mcp/__init__.py

Whitespace-only changes.

examples/hosted_mcp/approvals.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import argparse
2+
import asyncio
3+
4+
from agents import (
5+
Agent,
6+
HostedMCPTool,
7+
MCPToolApprovalFunctionResult,
8+
MCPToolApprovalRequest,
9+
Runner,
10+
)
11+
12+
"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with
13+
approval callbacks."""
14+
15+
16+
def approval_callback(request: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult:
17+
answer = input(f"Approve running the tool `{request.data.name}`? (y/n) ")
18+
result: MCPToolApprovalFunctionResult = {"approve": answer == "y"}
19+
if not result["approve"]:
20+
result["reason"] = "User denied"
21+
return result
22+
23+
24+
async def main(verbose: bool, stream: bool):
25+
agent = Agent(
26+
name="Assistant",
27+
tools=[
28+
HostedMCPTool(
29+
tool_config={
30+
"type": "mcp",
31+
"server_label": "gitmcp",
32+
"server_url": "https://gitmcp.io/openai/codex",
33+
"require_approval": "always",
34+
},
35+
on_approval_request=approval_callback,
36+
)
37+
],
38+
)
39+
40+
if stream:
41+
result = Runner.run_streamed(agent, "Which language is this repo written in?")
42+
async for event in result.stream_events():
43+
if event.type == "run_item_stream_event":
44+
print(f"Got event of type {event.item.__class__.__name__}")
45+
print(f"Done streaming; final result: {result.final_output}")
46+
else:
47+
res = await Runner.run(agent, "Which language is this repo written in?")
48+
print(res.final_output)
49+
50+
if verbose:
51+
for item in result.new_items:
52+
print(item)
53+
54+
55+
if __name__ == "__main__":
56+
parser = argparse.ArgumentParser()
57+
parser.add_argument("--verbose", action="store_true", default=False)
58+
parser.add_argument("--stream", action="store_true", default=False)
59+
args = parser.parse_args()
60+
61+
asyncio.run(main(args.verbose, args.stream))

examples/hosted_mcp/simple.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import argparse
2+
import asyncio
3+
4+
from agents import Agent, HostedMCPTool, Runner
5+
6+
"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with
7+
approvals not required for any tools. You should only use this for trusted MCP servers."""
8+
9+
10+
async def main(verbose: bool, stream: bool):
11+
agent = Agent(
12+
name="Assistant",
13+
tools=[
14+
HostedMCPTool(
15+
tool_config={
16+
"type": "mcp",
17+
"server_label": "gitmcp",
18+
"server_url": "https://gitmcp.io/openai/codex",
19+
"require_approval": "never",
20+
}
21+
)
22+
],
23+
)
24+
25+
if stream:
26+
result = Runner.run_streamed(agent, "Which language is this repo written in?")
27+
async for event in result.stream_events():
28+
if event.type == "run_item_stream_event":
29+
print(f"Got event of type {event.item.__class__.__name__}")
30+
print(f"Done streaming; final result: {result.final_output}")
31+
else:
32+
res = await Runner.run(agent, "Which language is this repo written in?")
33+
print(res.final_output)
34+
# The repository is primarily written in multiple languages, including Rust and TypeScript...
35+
36+
if verbose:
37+
for item in result.new_items:
38+
print(item)
39+
40+
41+
if __name__ == "__main__":
42+
parser = argparse.ArgumentParser()
43+
parser.add_argument("--verbose", action="store_true", default=False)
44+
parser.add_argument("--stream", action="store_true", default=False)
45+
args = parser.parse_args()
46+
47+
asyncio.run(main(args.verbose, args.stream))

src/agents/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@
5858
FileSearchTool,
5959
FunctionTool,
6060
FunctionToolResult,
61+
HostedMCPTool,
62+
MCPToolApprovalFunction,
63+
MCPToolApprovalFunctionResult,
64+
MCPToolApprovalRequest,
6165
Tool,
6266
WebSearchTool,
6367
default_tool_error_function,
@@ -208,6 +212,10 @@ def enable_verbose_stdout_logging():
208212
"FileSearchTool",
209213
"Tool",
210214
"WebSearchTool",
215+
"HostedMCPTool",
216+
"MCPToolApprovalFunction",
217+
"MCPToolApprovalRequest",
218+
"MCPToolApprovalFunctionResult",
211219
"function_tool",
212220
"Usage",
213221
"add_trace_processor",

src/agents/_run_impl.py

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
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
2930
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
3031

3132
from .agent import Agent, ToolsToFinalOutputResult
@@ -38,6 +39,9 @@
3839
HandoffCallItem,
3940
HandoffOutputItem,
4041
ItemHelpers,
42+
MCPApprovalRequestItem,
43+
MCPApprovalResponseItem,
44+
MCPListToolsItem,
4145
MessageOutputItem,
4246
ModelResponse,
4347
ReasoningItem,
@@ -52,7 +56,14 @@
5256
from .models.interface import ModelTracing
5357
from .run_context import RunContextWrapper, TContext
5458
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+
)
5667
from .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
116133
class 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

Comments
 (0)