Skip to content

Fix MCP tool name conflicts by adding server name prefixes #1178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions src/agents/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,26 @@ async def _apply_dynamic_tool_filter(

return filtered_tools

async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
"""Invoke a tool on the server.

Args:
tool_name: The name of the tool to call. This can be either the prefixed name
(server_name_tool_name) or the original tool name.
arguments: The arguments to pass to the tool.
"""
if not self.session:
raise UserError("Server not initialized. Make sure you call `connect()` first.")

# If the tool name is prefixed with server name, strip it
if tool_name.startswith(f"{self.name}_"):
# Remove the server name prefix and the underscore
original_tool_name = tool_name[len(self.name) + 1:]
else:
original_tool_name = tool_name

return await self.session.call_tool(original_tool_name, arguments)

@abc.abstractmethod
def create_streams(
self,
Expand Down Expand Up @@ -275,8 +295,14 @@ async def list_tools(
# Reset the cache dirty to False
self._cache_dirty = False
# Fetch the tools from the server
self._tools_list = (await self.session.list_tools()).tools
tools = self._tools_list
tools = (await self.session.list_tools()).tools
# Add server name prefix to each tool's name to ensure global uniqueness
for tool in tools:
# Store original name for actual tool calls
tool.original_name = tool.name # type: ignore[attr-defined]
# Prefix tool name with server name using underscore separator
tool.name = f"{self.name}_{tool.name}"
self._tools_list = tools

# Filter tools based on tool_filter
filtered_tools = tools
Expand All @@ -286,13 +312,6 @@ async def list_tools(
filtered_tools = await self._apply_tool_filter(filtered_tools, run_context, agent)
return filtered_tools

async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
"""Invoke a tool on the server."""
if not self.session:
raise UserError("Server not initialized. Make sure you call `connect()` first.")

return await self.session.call_tool(tool_name, arguments)

async def list_prompts(
self,
) -> ListPromptsResult:
Expand Down
16 changes: 11 additions & 5 deletions src/agents/mcp/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,21 +178,27 @@ async def invoke_mcp_tool(
f"Invalid JSON input for tool {tool.name}: {input_json}"
) from e

# Use original tool name for server call (strip server prefix if present)
original_name = getattr(tool, "original_name", tool.name)

if _debug.DONT_LOG_TOOL_DATA:
logger.debug(f"Invoking MCP tool {tool.name}")
logger.debug(f"Invoking MCP tool {tool.name} (original: {original_name})")
else:
logger.debug(f"Invoking MCP tool {tool.name} with input {input_json}")
logger.debug(
f"Invoking MCP tool {tool.name} (original: {original_name}) "
f"with input {input_json}"
)

try:
result = await server.call_tool(tool.name, json_data)
result = await server.call_tool(original_name, json_data)
except Exception as e:
logger.error(f"Error invoking MCP tool {tool.name}: {e}")
raise AgentsException(f"Error invoking MCP tool {tool.name}: {e}") from e

if _debug.DONT_LOG_TOOL_DATA:
logger.debug(f"MCP tool {tool.name} completed.")
logger.debug(f"MCP tool {tool.name} (original: {original_name}) completed.")
else:
logger.debug(f"MCP tool {tool.name} returned {result}")
logger.debug(f"MCP tool {tool.name} (original: {original_name}) returned {result}")

# The MCP tool result is a list of content items, whereas OpenAI tool outputs are a single
# string. We'll try to convert.
Expand Down
24 changes: 13 additions & 11 deletions src/agents/models/chatcmpl_stream_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,10 +288,11 @@ async def handle_stream(
function_call = state.function_calls[tc_delta.index]

# Start streaming as soon as we have function name and call_id
if (not state.function_call_streaming[tc_delta.index] and
function_call.name and
function_call.call_id):

if (
not state.function_call_streaming[tc_delta.index]
and function_call.name
and function_call.call_id
):
# Calculate the output index for this function call
function_call_starting_index = 0
if state.reasoning_content_index_and_output:
Expand All @@ -308,9 +309,9 @@ async def handle_stream(

# Mark this function call as streaming and store its output index
state.function_call_streaming[tc_delta.index] = True
state.function_call_output_idx[
tc_delta.index
] = function_call_starting_index
state.function_call_output_idx[tc_delta.index] = (
function_call_starting_index
)

# Send initial function call added event
yield ResponseOutputItemAddedEvent(
Expand All @@ -327,10 +328,11 @@ async def handle_stream(
)

# Stream arguments if we've started streaming this function call
if (state.function_call_streaming.get(tc_delta.index, False) and
tc_function and
tc_function.arguments):

if (
state.function_call_streaming.get(tc_delta.index, False)
and tc_function
and tc_function.arguments
):
output_index = state.function_call_output_idx[tc_delta.index]
yield ResponseFunctionCallArgumentsDeltaEvent(
delta=tc_function.arguments,
Expand Down
Loading
Loading