Skip to content

Commit 48fad9e

Browse files
committed
Merge branch 'main' of https://github.com/openai/openai-agents-python into feat/draw_graph
2 parents 9d04671 + 4f8cbfa commit 48fad9e

20 files changed

+1233
-36
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies = [
1313
"typing-extensions>=4.12.2, <5",
1414
"requests>=2.0, <3",
1515
"types-requests>=2.0, <3",
16+
"mcp; python_version >= '3.10'",
1617
]
1718
classifiers = [
1819
"Typing :: Typed",

src/agents/_run_impl.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ def process_model_response(
344344
cls,
345345
*,
346346
agent: Agent[Any],
347+
all_tools: list[Tool],
347348
response: ModelResponse,
348349
output_schema: AgentOutputSchema | None,
349350
handoffs: list[Handoff],
@@ -355,8 +356,8 @@ def process_model_response(
355356
computer_actions = []
356357

357358
handoff_map = {handoff.tool_name: handoff for handoff in handoffs}
358-
function_map = {tool.name: tool for tool in agent.tools if isinstance(tool, FunctionTool)}
359-
computer_tool = next((tool for tool in agent.tools if isinstance(tool, ComputerTool)), None)
359+
function_map = {tool.name: tool for tool in all_tools if isinstance(tool, FunctionTool)}
360+
computer_tool = next((tool for tool in all_tools if isinstance(tool, ComputerTool)), None)
360361

361362
for output in response.output:
362363
if isinstance(output, ResponseOutputMessage):

src/agents/agent.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .handoffs import Handoff
1313
from .items import ItemHelpers
1414
from .logger import logger
15+
from .mcp import MCPUtil
1516
from .model_settings import ModelSettings
1617
from .models.interface import Model
1718
from .run_context import RunContextWrapper, TContext
@@ -21,6 +22,7 @@
2122

2223
if TYPE_CHECKING:
2324
from .lifecycle import AgentHooks
25+
from .mcp import MCPServer
2426
from .result import RunResult
2527

2628

@@ -107,6 +109,16 @@ class Agent(Generic[TContext]):
107109
tools: list[Tool] = field(default_factory=list)
108110
"""A list of tools that the agent can use."""
109111

112+
mcp_servers: list[MCPServer] = field(default_factory=list)
113+
"""A list of [Model Context Protocol](https://modelcontextprotocol.io/) servers that
114+
the agent can use. Every time the agent runs, it will include tools from these servers in the
115+
list of available tools.
116+
117+
NOTE: You are expected to manage the lifecycle of these servers. Specifically, you must call
118+
`server.connect()` before passing it to the agent, and `server.cleanup()` when the server is no
119+
longer needed.
120+
"""
121+
110122
input_guardrails: list[InputGuardrail[TContext]] = field(default_factory=list)
111123
"""A list of checks that run in parallel to the agent's execution, before generating a
112124
response. Runs only if the agent is the first agent in the chain.
@@ -205,3 +217,11 @@ async def get_system_prompt(self, run_context: RunContextWrapper[TContext]) -> s
205217
logger.error(f"Instructions must be a string or a function, got {self.instructions}")
206218

207219
return None
220+
221+
async def get_mcp_tools(self) -> list[Tool]:
222+
"""Fetches the available tools from the MCP servers."""
223+
return await MCPUtil.get_all_function_tools(self.mcp_servers)
224+
225+
async def get_all_tools(self) -> list[Tool]:
226+
"""All agent tools, including MCP tools and function tools."""
227+
return await MCPUtil.get_all_function_tools(self.mcp_servers) + self.tools

src/agents/mcp/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
try:
2+
from .server import (
3+
MCPServer,
4+
MCPServerSse,
5+
MCPServerSseParams,
6+
MCPServerStdio,
7+
MCPServerStdioParams,
8+
)
9+
except ImportError:
10+
pass
11+
12+
from .util import MCPUtil
13+
14+
__all__ = [
15+
"MCPServer",
16+
"MCPServerSse",
17+
"MCPServerSseParams",
18+
"MCPServerStdio",
19+
"MCPServerStdioParams",
20+
"MCPUtil",
21+
]

src/agents/mcp/mcp_util.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import functools
2+
import json
3+
from typing import Any
4+
5+
from mcp.types import Tool as MCPTool
6+
7+
from .. import _debug
8+
from ..exceptions import AgentsException, ModelBehaviorError, UserError
9+
from ..logger import logger
10+
from ..run_context import RunContextWrapper
11+
from ..tool import FunctionTool, Tool
12+
from .server import MCPServer
13+
14+
15+
class MCPUtil:
16+
"""Set of utilities for interop between MCP and Agents SDK tools."""
17+
18+
@classmethod
19+
async def get_all_function_tools(cls, servers: list[MCPServer]) -> list[Tool]:
20+
"""Get all function tools from a list of MCP servers."""
21+
tools = []
22+
tool_names: set[str] = set()
23+
for server in servers:
24+
server_tools = await cls.get_function_tools(server)
25+
server_tool_names = {tool.name for tool in server_tools}
26+
if len(server_tool_names & tool_names) > 0:
27+
raise UserError(
28+
f"Duplicate tool names found across MCP servers: "
29+
f"{server_tool_names & tool_names}"
30+
)
31+
tool_names.update(server_tool_names)
32+
tools.extend(server_tools)
33+
34+
return tools
35+
36+
@classmethod
37+
async def get_function_tools(cls, server: MCPServer) -> list[Tool]:
38+
"""Get all function tools from a single MCP server."""
39+
tools = await server.list_tools()
40+
return [cls.to_function_tool(tool, server) for tool in tools]
41+
42+
@classmethod
43+
def to_function_tool(cls, tool: MCPTool, server: MCPServer) -> FunctionTool:
44+
"""Convert an MCP tool to an Agents SDK function tool."""
45+
invoke_func = functools.partial(cls.invoke_mcp_tool, server, tool)
46+
return FunctionTool(
47+
name=tool.name,
48+
description=tool.description or "",
49+
params_json_schema=tool.inputSchema,
50+
on_invoke_tool=invoke_func,
51+
strict_json_schema=False,
52+
)
53+
54+
@classmethod
55+
async def invoke_mcp_tool(
56+
cls, server: MCPServer, tool: MCPTool, context: RunContextWrapper[Any], input_json: str
57+
) -> str:
58+
"""Invoke an MCP tool and return the result as a string."""
59+
try:
60+
json_data: dict[str, Any] = json.loads(input_json) if input_json else {}
61+
except Exception as e:
62+
if _debug.DONT_LOG_TOOL_DATA:
63+
logger.debug(f"Invalid JSON input for tool {tool.name}")
64+
else:
65+
logger.debug(f"Invalid JSON input for tool {tool.name}: {input_json}")
66+
raise ModelBehaviorError(
67+
f"Invalid JSON input for tool {tool.name}: {input_json}"
68+
) from e
69+
70+
if _debug.DONT_LOG_TOOL_DATA:
71+
logger.debug(f"Invoking MCP tool {tool.name}")
72+
else:
73+
logger.debug(f"Invoking MCP tool {tool.name} with input {input_json}")
74+
75+
try:
76+
result = await server.call_tool(tool.name, json_data)
77+
except Exception as e:
78+
logger.error(f"Error invoking MCP tool {tool.name}: {e}")
79+
raise AgentsException(f"Error invoking MCP tool {tool.name}: {e}") from e
80+
81+
if _debug.DONT_LOG_TOOL_DATA:
82+
logger.debug(f"MCP tool {tool.name} completed.")
83+
else:
84+
logger.debug(f"MCP tool {tool.name} returned {result}")
85+
86+
# The MCP tool result is a list of content items, whereas OpenAI tool outputs are a single
87+
# string. We'll try to convert.
88+
if len(result.content) == 1:
89+
return result.content[0].model_dump_json()
90+
elif len(result.content) > 1:
91+
return json.dumps([item.model_dump() for item in result.content])
92+
else:
93+
logger.error(f"Errored MCP tool result: {result}")
94+
return "Error running tool."

0 commit comments

Comments
 (0)