Hosted MCP support (#731)
--- [//]: # (BEGIN SAPLING FOOTER) * #732 * __->__ #731
This commit is contained in:
parent
ce2e2a4571
commit
9fa5c39d69
9 changed files with 332 additions and 11 deletions
0
examples/hosted_mcp/__init__.py
Normal file
0
examples/hosted_mcp/__init__.py
Normal file
61
examples/hosted_mcp/approvals.py
Normal file
61
examples/hosted_mcp/approvals.py
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from agents import (
|
||||||
|
Agent,
|
||||||
|
HostedMCPTool,
|
||||||
|
MCPToolApprovalFunctionResult,
|
||||||
|
MCPToolApprovalRequest,
|
||||||
|
Runner,
|
||||||
|
)
|
||||||
|
|
||||||
|
"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with
|
||||||
|
approval callbacks."""
|
||||||
|
|
||||||
|
|
||||||
|
def approval_callback(request: MCPToolApprovalRequest) -> MCPToolApprovalFunctionResult:
|
||||||
|
answer = input(f"Approve running the tool `{request.data.name}`? (y/n) ")
|
||||||
|
result: MCPToolApprovalFunctionResult = {"approve": answer == "y"}
|
||||||
|
if not result["approve"]:
|
||||||
|
result["reason"] = "User denied"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def main(verbose: bool, stream: bool):
|
||||||
|
agent = Agent(
|
||||||
|
name="Assistant",
|
||||||
|
tools=[
|
||||||
|
HostedMCPTool(
|
||||||
|
tool_config={
|
||||||
|
"type": "mcp",
|
||||||
|
"server_label": "gitmcp",
|
||||||
|
"server_url": "https://gitmcp.io/openai/codex",
|
||||||
|
"require_approval": "always",
|
||||||
|
},
|
||||||
|
on_approval_request=approval_callback,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if stream:
|
||||||
|
result = Runner.run_streamed(agent, "Which language is this repo written in?")
|
||||||
|
async for event in result.stream_events():
|
||||||
|
if event.type == "run_item_stream_event":
|
||||||
|
print(f"Got event of type {event.item.__class__.__name__}")
|
||||||
|
print(f"Done streaming; final result: {result.final_output}")
|
||||||
|
else:
|
||||||
|
res = await Runner.run(agent, "Which language is this repo written in?")
|
||||||
|
print(res.final_output)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
for item in result.new_items:
|
||||||
|
print(item)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--verbose", action="store_true", default=False)
|
||||||
|
parser.add_argument("--stream", action="store_true", default=False)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
asyncio.run(main(args.verbose, args.stream))
|
||||||
47
examples/hosted_mcp/simple.py
Normal file
47
examples/hosted_mcp/simple.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from agents import Agent, HostedMCPTool, Runner
|
||||||
|
|
||||||
|
"""This example demonstrates how to use the hosted MCP support in the OpenAI Responses API, with
|
||||||
|
approvals not required for any tools. You should only use this for trusted MCP servers."""
|
||||||
|
|
||||||
|
|
||||||
|
async def main(verbose: bool, stream: bool):
|
||||||
|
agent = Agent(
|
||||||
|
name="Assistant",
|
||||||
|
tools=[
|
||||||
|
HostedMCPTool(
|
||||||
|
tool_config={
|
||||||
|
"type": "mcp",
|
||||||
|
"server_label": "gitmcp",
|
||||||
|
"server_url": "https://gitmcp.io/openai/codex",
|
||||||
|
"require_approval": "never",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
if stream:
|
||||||
|
result = Runner.run_streamed(agent, "Which language is this repo written in?")
|
||||||
|
async for event in result.stream_events():
|
||||||
|
if event.type == "run_item_stream_event":
|
||||||
|
print(f"Got event of type {event.item.__class__.__name__}")
|
||||||
|
print(f"Done streaming; final result: {result.final_output}")
|
||||||
|
else:
|
||||||
|
res = await Runner.run(agent, "Which language is this repo written in?")
|
||||||
|
print(res.final_output)
|
||||||
|
# The repository is primarily written in multiple languages, including Rust and TypeScript...
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
for item in result.new_items:
|
||||||
|
print(item)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--verbose", action="store_true", default=False)
|
||||||
|
parser.add_argument("--stream", action="store_true", default=False)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
asyncio.run(main(args.verbose, args.stream))
|
||||||
|
|
@ -58,6 +58,10 @@ from .tool import (
|
||||||
FileSearchTool,
|
FileSearchTool,
|
||||||
FunctionTool,
|
FunctionTool,
|
||||||
FunctionToolResult,
|
FunctionToolResult,
|
||||||
|
HostedMCPTool,
|
||||||
|
MCPToolApprovalFunction,
|
||||||
|
MCPToolApprovalFunctionResult,
|
||||||
|
MCPToolApprovalRequest,
|
||||||
Tool,
|
Tool,
|
||||||
WebSearchTool,
|
WebSearchTool,
|
||||||
default_tool_error_function,
|
default_tool_error_function,
|
||||||
|
|
@ -208,6 +212,10 @@ __all__ = [
|
||||||
"FileSearchTool",
|
"FileSearchTool",
|
||||||
"Tool",
|
"Tool",
|
||||||
"WebSearchTool",
|
"WebSearchTool",
|
||||||
|
"HostedMCPTool",
|
||||||
|
"MCPToolApprovalFunction",
|
||||||
|
"MCPToolApprovalRequest",
|
||||||
|
"MCPToolApprovalFunctionResult",
|
||||||
"function_tool",
|
"function_tool",
|
||||||
"Usage",
|
"Usage",
|
||||||
"add_trace_processor",
|
"add_trace_processor",
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ from openai.types.responses.response_computer_tool_call import (
|
||||||
ActionType,
|
ActionType,
|
||||||
ActionWait,
|
ActionWait,
|
||||||
)
|
)
|
||||||
from openai.types.responses.response_input_param import ComputerCallOutput
|
from openai.types.responses.response_input_param import ComputerCallOutput, McpApprovalResponse
|
||||||
|
from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools
|
||||||
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
||||||
|
|
||||||
from .agent import Agent, ToolsToFinalOutputResult
|
from .agent import Agent, ToolsToFinalOutputResult
|
||||||
|
|
@ -38,6 +39,9 @@ from .items import (
|
||||||
HandoffCallItem,
|
HandoffCallItem,
|
||||||
HandoffOutputItem,
|
HandoffOutputItem,
|
||||||
ItemHelpers,
|
ItemHelpers,
|
||||||
|
MCPApprovalRequestItem,
|
||||||
|
MCPApprovalResponseItem,
|
||||||
|
MCPListToolsItem,
|
||||||
MessageOutputItem,
|
MessageOutputItem,
|
||||||
ModelResponse,
|
ModelResponse,
|
||||||
ReasoningItem,
|
ReasoningItem,
|
||||||
|
|
@ -52,7 +56,14 @@ from .model_settings import ModelSettings
|
||||||
from .models.interface import ModelTracing
|
from .models.interface import ModelTracing
|
||||||
from .run_context import RunContextWrapper, TContext
|
from .run_context import RunContextWrapper, TContext
|
||||||
from .stream_events import RunItemStreamEvent, StreamEvent
|
from .stream_events import RunItemStreamEvent, StreamEvent
|
||||||
from .tool import ComputerTool, FunctionTool, FunctionToolResult, Tool
|
from .tool import (
|
||||||
|
ComputerTool,
|
||||||
|
FunctionTool,
|
||||||
|
FunctionToolResult,
|
||||||
|
HostedMCPTool,
|
||||||
|
MCPToolApprovalRequest,
|
||||||
|
Tool,
|
||||||
|
)
|
||||||
from .tracing import (
|
from .tracing import (
|
||||||
SpanError,
|
SpanError,
|
||||||
Trace,
|
Trace,
|
||||||
|
|
@ -112,6 +123,12 @@ class ToolRunComputerAction:
|
||||||
computer_tool: ComputerTool
|
computer_tool: ComputerTool
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ToolRunMCPApprovalRequest:
|
||||||
|
request_item: McpApprovalRequest
|
||||||
|
mcp_tool: HostedMCPTool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProcessedResponse:
|
class ProcessedResponse:
|
||||||
new_items: list[RunItem]
|
new_items: list[RunItem]
|
||||||
|
|
@ -119,8 +136,9 @@ class ProcessedResponse:
|
||||||
functions: list[ToolRunFunction]
|
functions: list[ToolRunFunction]
|
||||||
computer_actions: list[ToolRunComputerAction]
|
computer_actions: list[ToolRunComputerAction]
|
||||||
tools_used: list[str] # Names of all tools used, including hosted tools
|
tools_used: list[str] # Names of all tools used, including hosted tools
|
||||||
|
mcp_approval_requests: list[ToolRunMCPApprovalRequest] # Only requests with callbacks
|
||||||
|
|
||||||
def has_tools_to_run(self) -> bool:
|
def has_tools_or_approvals_to_run(self) -> bool:
|
||||||
# Handoffs, functions and computer actions need local processing
|
# Handoffs, functions and computer actions need local processing
|
||||||
# Hosted tools have already run, so there's nothing to do.
|
# Hosted tools have already run, so there's nothing to do.
|
||||||
return any(
|
return any(
|
||||||
|
|
@ -128,6 +146,7 @@ class ProcessedResponse:
|
||||||
self.handoffs,
|
self.handoffs,
|
||||||
self.functions,
|
self.functions,
|
||||||
self.computer_actions,
|
self.computer_actions,
|
||||||
|
self.mcp_approval_requests,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -226,7 +245,16 @@ class RunImpl:
|
||||||
new_step_items.extend([result.run_item for result in function_results])
|
new_step_items.extend([result.run_item for result in function_results])
|
||||||
new_step_items.extend(computer_results)
|
new_step_items.extend(computer_results)
|
||||||
|
|
||||||
# Second, check if there are any handoffs
|
# Next, run the MCP approval requests
|
||||||
|
if processed_response.mcp_approval_requests:
|
||||||
|
approval_results = await cls.execute_mcp_approval_requests(
|
||||||
|
agent=agent,
|
||||||
|
approval_requests=processed_response.mcp_approval_requests,
|
||||||
|
context_wrapper=context_wrapper,
|
||||||
|
)
|
||||||
|
new_step_items.extend(approval_results)
|
||||||
|
|
||||||
|
# Next, check if there are any handoffs
|
||||||
if run_handoffs := processed_response.handoffs:
|
if run_handoffs := processed_response.handoffs:
|
||||||
return await cls.execute_handoffs(
|
return await cls.execute_handoffs(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
|
|
@ -240,7 +268,7 @@ class RunImpl:
|
||||||
run_config=run_config,
|
run_config=run_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Third, we'll check if the tool use should result in a final output
|
# Next, we'll check if the tool use should result in a final output
|
||||||
check_tool_use = await cls._check_for_final_output_from_tools(
|
check_tool_use = await cls._check_for_final_output_from_tools(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
tool_results=function_results,
|
tool_results=function_results,
|
||||||
|
|
@ -295,7 +323,7 @@ class RunImpl:
|
||||||
)
|
)
|
||||||
elif (
|
elif (
|
||||||
not output_schema or output_schema.is_plain_text()
|
not output_schema or output_schema.is_plain_text()
|
||||||
) and not processed_response.has_tools_to_run():
|
) and not processed_response.has_tools_or_approvals_to_run():
|
||||||
return await cls.execute_final_output(
|
return await cls.execute_final_output(
|
||||||
agent=agent,
|
agent=agent,
|
||||||
original_input=original_input,
|
original_input=original_input,
|
||||||
|
|
@ -343,10 +371,16 @@ class RunImpl:
|
||||||
run_handoffs = []
|
run_handoffs = []
|
||||||
functions = []
|
functions = []
|
||||||
computer_actions = []
|
computer_actions = []
|
||||||
|
mcp_approval_requests = []
|
||||||
tools_used: list[str] = []
|
tools_used: list[str] = []
|
||||||
handoff_map = {handoff.tool_name: handoff for handoff in handoffs}
|
handoff_map = {handoff.tool_name: handoff for handoff in handoffs}
|
||||||
function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)}
|
function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)}
|
||||||
computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None)
|
computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None)
|
||||||
|
hosted_mcp_server_map = {
|
||||||
|
tool.tool_config["server_label"]: tool
|
||||||
|
for tool in all_tools
|
||||||
|
if isinstance(tool, HostedMCPTool)
|
||||||
|
}
|
||||||
|
|
||||||
for output in response.output:
|
for output in response.output:
|
||||||
if isinstance(output, ResponseOutputMessage):
|
if isinstance(output, ResponseOutputMessage):
|
||||||
|
|
@ -375,6 +409,34 @@ class RunImpl:
|
||||||
computer_actions.append(
|
computer_actions.append(
|
||||||
ToolRunComputerAction(tool_call=output, computer_tool=computer_tool)
|
ToolRunComputerAction(tool_call=output, computer_tool=computer_tool)
|
||||||
)
|
)
|
||||||
|
elif isinstance(output, McpApprovalRequest):
|
||||||
|
items.append(MCPApprovalRequestItem(raw_item=output, agent=agent))
|
||||||
|
if output.server_label not in hosted_mcp_server_map:
|
||||||
|
_error_tracing.attach_error_to_current_span(
|
||||||
|
SpanError(
|
||||||
|
message="MCP server label not found",
|
||||||
|
data={"server_label": output.server_label},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise ModelBehaviorError(f"MCP server label {output.server_label} not found")
|
||||||
|
else:
|
||||||
|
server = hosted_mcp_server_map[output.server_label]
|
||||||
|
if server.on_approval_request:
|
||||||
|
mcp_approval_requests.append(
|
||||||
|
ToolRunMCPApprovalRequest(
|
||||||
|
request_item=output,
|
||||||
|
mcp_tool=server,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"MCP server {output.server_label} has no on_approval_request hook"
|
||||||
|
)
|
||||||
|
elif isinstance(output, McpListTools):
|
||||||
|
items.append(MCPListToolsItem(raw_item=output, agent=agent))
|
||||||
|
elif isinstance(output, McpCall):
|
||||||
|
items.append(ToolCallItem(raw_item=output, agent=agent))
|
||||||
|
tools_used.append(output.name)
|
||||||
elif not isinstance(output, ResponseFunctionToolCall):
|
elif not isinstance(output, ResponseFunctionToolCall):
|
||||||
logger.warning(f"Unexpected output type, ignoring: {type(output)}")
|
logger.warning(f"Unexpected output type, ignoring: {type(output)}")
|
||||||
continue
|
continue
|
||||||
|
|
@ -417,6 +479,7 @@ class RunImpl:
|
||||||
functions=functions,
|
functions=functions,
|
||||||
computer_actions=computer_actions,
|
computer_actions=computer_actions,
|
||||||
tools_used=tools_used,
|
tools_used=tools_used,
|
||||||
|
mcp_approval_requests=mcp_approval_requests,
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -643,6 +706,40 @@ class RunImpl:
|
||||||
next_step=NextStepHandoff(new_agent),
|
next_step=NextStepHandoff(new_agent),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute_mcp_approval_requests(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
agent: Agent[TContext],
|
||||||
|
approval_requests: list[ToolRunMCPApprovalRequest],
|
||||||
|
context_wrapper: RunContextWrapper[TContext],
|
||||||
|
) -> list[RunItem]:
|
||||||
|
async def run_single_approval(approval_request: ToolRunMCPApprovalRequest) -> RunItem:
|
||||||
|
callback = approval_request.mcp_tool.on_approval_request
|
||||||
|
assert callback is not None, "Callback is required for MCP approval requests"
|
||||||
|
maybe_awaitable_result = callback(
|
||||||
|
MCPToolApprovalRequest(context_wrapper, approval_request.request_item)
|
||||||
|
)
|
||||||
|
if inspect.isawaitable(maybe_awaitable_result):
|
||||||
|
result = await maybe_awaitable_result
|
||||||
|
else:
|
||||||
|
result = maybe_awaitable_result
|
||||||
|
reason = result.get("reason", None)
|
||||||
|
raw_item: McpApprovalResponse = {
|
||||||
|
"approval_request_id": approval_request.request_item.id,
|
||||||
|
"approve": result["approve"],
|
||||||
|
"type": "mcp_approval_response",
|
||||||
|
}
|
||||||
|
if not result["approve"] and reason:
|
||||||
|
raw_item["reason"] = reason
|
||||||
|
return MCPApprovalResponseItem(
|
||||||
|
raw_item=raw_item,
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
tasks = [run_single_approval(approval_request) for approval_request in approval_requests]
|
||||||
|
return await asyncio.gather(*tasks)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def execute_final_output(
|
async def execute_final_output(
|
||||||
cls,
|
cls,
|
||||||
|
|
@ -727,6 +824,11 @@ class RunImpl:
|
||||||
event = RunItemStreamEvent(item=item, name="tool_output")
|
event = RunItemStreamEvent(item=item, name="tool_output")
|
||||||
elif isinstance(item, ReasoningItem):
|
elif isinstance(item, ReasoningItem):
|
||||||
event = RunItemStreamEvent(item=item, name="reasoning_item_created")
|
event = RunItemStreamEvent(item=item, name="reasoning_item_created")
|
||||||
|
elif isinstance(item, MCPApprovalRequestItem):
|
||||||
|
event = RunItemStreamEvent(item=item, name="mcp_approval_requested")
|
||||||
|
elif isinstance(item, MCPListToolsItem):
|
||||||
|
event = RunItemStreamEvent(item=item, name="mcp_list_tools")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Unexpected item type: {type(item)}")
|
logger.warning(f"Unexpected item type: {type(item)}")
|
||||||
event = None
|
event = None
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,12 @@ from openai.types.responses import (
|
||||||
ResponseOutputText,
|
ResponseOutputText,
|
||||||
ResponseStreamEvent,
|
ResponseStreamEvent,
|
||||||
)
|
)
|
||||||
from openai.types.responses.response_input_item_param import ComputerCallOutput, FunctionCallOutput
|
from openai.types.responses.response_input_item_param import (
|
||||||
|
ComputerCallOutput,
|
||||||
|
FunctionCallOutput,
|
||||||
|
McpApprovalResponse,
|
||||||
|
)
|
||||||
|
from openai.types.responses.response_output_item import McpApprovalRequest, McpCall, McpListTools
|
||||||
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
from openai.types.responses.response_reasoning_item import ResponseReasoningItem
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing_extensions import TypeAlias
|
from typing_extensions import TypeAlias
|
||||||
|
|
@ -108,6 +113,7 @@ ToolCallItemTypes: TypeAlias = Union[
|
||||||
ResponseComputerToolCall,
|
ResponseComputerToolCall,
|
||||||
ResponseFileSearchToolCall,
|
ResponseFileSearchToolCall,
|
||||||
ResponseFunctionWebSearch,
|
ResponseFunctionWebSearch,
|
||||||
|
McpCall,
|
||||||
]
|
]
|
||||||
"""A type that represents a tool call item."""
|
"""A type that represents a tool call item."""
|
||||||
|
|
||||||
|
|
@ -147,6 +153,36 @@ class ReasoningItem(RunItemBase[ResponseReasoningItem]):
|
||||||
type: Literal["reasoning_item"] = "reasoning_item"
|
type: Literal["reasoning_item"] = "reasoning_item"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MCPListToolsItem(RunItemBase[McpListTools]):
|
||||||
|
"""Represents a call to an MCP server to list tools."""
|
||||||
|
|
||||||
|
raw_item: McpListTools
|
||||||
|
"""The raw MCP list tools call."""
|
||||||
|
|
||||||
|
type: Literal["mcp_list_tools_item"] = "mcp_list_tools_item"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MCPApprovalRequestItem(RunItemBase[McpApprovalRequest]):
|
||||||
|
"""Represents a request for MCP approval."""
|
||||||
|
|
||||||
|
raw_item: McpApprovalRequest
|
||||||
|
"""The raw MCP approval request."""
|
||||||
|
|
||||||
|
type: Literal["mcp_approval_request_item"] = "mcp_approval_request_item"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MCPApprovalResponseItem(RunItemBase[McpApprovalResponse]):
|
||||||
|
"""Represents a response to an MCP approval request."""
|
||||||
|
|
||||||
|
raw_item: McpApprovalResponse
|
||||||
|
"""The raw MCP approval response."""
|
||||||
|
|
||||||
|
type: Literal["mcp_approval_response_item"] = "mcp_approval_response_item"
|
||||||
|
|
||||||
|
|
||||||
RunItem: TypeAlias = Union[
|
RunItem: TypeAlias = Union[
|
||||||
MessageOutputItem,
|
MessageOutputItem,
|
||||||
HandoffCallItem,
|
HandoffCallItem,
|
||||||
|
|
@ -154,6 +190,9 @@ RunItem: TypeAlias = Union[
|
||||||
ToolCallItem,
|
ToolCallItem,
|
||||||
ToolCallOutputItem,
|
ToolCallOutputItem,
|
||||||
ReasoningItem,
|
ReasoningItem,
|
||||||
|
MCPListToolsItem,
|
||||||
|
MCPApprovalRequestItem,
|
||||||
|
MCPApprovalResponseItem,
|
||||||
]
|
]
|
||||||
"""An item generated by an agent."""
|
"""An item generated by an agent."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ from ..exceptions import UserError
|
||||||
from ..handoffs import Handoff
|
from ..handoffs import Handoff
|
||||||
from ..items import ItemHelpers, ModelResponse, TResponseInputItem
|
from ..items import ItemHelpers, ModelResponse, TResponseInputItem
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from ..tool import ComputerTool, FileSearchTool, FunctionTool, Tool, WebSearchTool
|
from ..tool import ComputerTool, FileSearchTool, FunctionTool, HostedMCPTool, Tool, WebSearchTool
|
||||||
from ..tracing import SpanError, response_span
|
from ..tracing import SpanError, response_span
|
||||||
from ..usage import Usage
|
from ..usage import Usage
|
||||||
from ..version import __version__
|
from ..version import __version__
|
||||||
|
|
@ -383,7 +383,9 @@ class Converter:
|
||||||
"display_height": tool.computer.dimensions[1],
|
"display_height": tool.computer.dimensions[1],
|
||||||
}
|
}
|
||||||
includes = None
|
includes = None
|
||||||
|
elif isinstance(tool, HostedMCPTool):
|
||||||
|
converted_tool = tool.tool_config
|
||||||
|
includes = None
|
||||||
else:
|
else:
|
||||||
raise UserError(f"Unknown tool type: {type(tool)}, tool")
|
raise UserError(f"Unknown tool type: {type(tool)}, tool")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ class RunItemStreamEvent:
|
||||||
"tool_called",
|
"tool_called",
|
||||||
"tool_output",
|
"tool_output",
|
||||||
"reasoning_item_created",
|
"reasoning_item_created",
|
||||||
|
"mcp_approval_requested",
|
||||||
|
"mcp_list_tools",
|
||||||
]
|
]
|
||||||
"""The name of the event."""
|
"""The name of the event."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Literal, Union, overload
|
from typing import Any, Callable, Literal, Union, overload
|
||||||
|
|
||||||
from openai.types.responses.file_search_tool_param import Filters, RankingOptions
|
from openai.types.responses.file_search_tool_param import Filters, RankingOptions
|
||||||
|
from openai.types.responses.response_output_item import McpApprovalRequest
|
||||||
|
from openai.types.responses.tool_param import Mcp
|
||||||
from openai.types.responses.web_search_tool_param import UserLocation
|
from openai.types.responses.web_search_tool_param import UserLocation
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
from typing_extensions import Concatenate, ParamSpec
|
from typing_extensions import Concatenate, NotRequired, ParamSpec, TypedDict
|
||||||
|
|
||||||
from . import _debug
|
from . import _debug
|
||||||
from .computer import AsyncComputer, Computer
|
from .computer import AsyncComputer, Computer
|
||||||
|
|
@ -130,7 +132,55 @@ class ComputerTool:
|
||||||
return "computer_use_preview"
|
return "computer_use_preview"
|
||||||
|
|
||||||
|
|
||||||
Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool]
|
@dataclass
|
||||||
|
class MCPToolApprovalRequest:
|
||||||
|
"""A request to approve a tool call."""
|
||||||
|
|
||||||
|
ctx_wrapper: RunContextWrapper[Any]
|
||||||
|
"""The run context."""
|
||||||
|
|
||||||
|
data: McpApprovalRequest
|
||||||
|
"""The data from the MCP tool approval request."""
|
||||||
|
|
||||||
|
|
||||||
|
class MCPToolApprovalFunctionResult(TypedDict):
|
||||||
|
"""The result of an MCP tool approval function."""
|
||||||
|
|
||||||
|
approve: bool
|
||||||
|
"""Whether to approve the tool call."""
|
||||||
|
|
||||||
|
reason: NotRequired[str]
|
||||||
|
"""An optional reason, if rejected."""
|
||||||
|
|
||||||
|
|
||||||
|
MCPToolApprovalFunction = Callable[
|
||||||
|
[MCPToolApprovalRequest], MaybeAwaitable[MCPToolApprovalFunctionResult]
|
||||||
|
]
|
||||||
|
"""A function that approves or rejects a tool call."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class HostedMCPTool:
|
||||||
|
"""A tool that allows the LLM to use a remote MCP server. The LLM will automatically list and
|
||||||
|
call tools, without requiring a a round trip back to your code.
|
||||||
|
If you want to run MCP servers locally via stdio, in a VPC or other non-publicly-accessible
|
||||||
|
environment, or you just prefer to run tool calls locally, then you can instead use the servers
|
||||||
|
in `agents.mcp` and pass `Agent(mcp_servers=[...])` to the agent."""
|
||||||
|
|
||||||
|
tool_config: Mcp
|
||||||
|
"""The MCP tool config, which includes the server URL and other settings."""
|
||||||
|
|
||||||
|
on_approval_request: MCPToolApprovalFunction | None = None
|
||||||
|
"""An optional function that will be called if approval is requested for an MCP tool. If not
|
||||||
|
provided, you will need to manually add approvals/rejections to the input and call
|
||||||
|
`Runner.run(...)` again."""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "hosted_mcp"
|
||||||
|
|
||||||
|
|
||||||
|
Tool = Union[FunctionTool, FileSearchTool, WebSearchTool, ComputerTool, HostedMCPTool]
|
||||||
"""A tool that can be used in an agent."""
|
"""A tool that can be used in an agent."""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -308,3 +358,13 @@ def function_tool(
|
||||||
return _create_function_tool(real_func)
|
return _create_function_tool(real_func)
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
return decorator
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue