Skip to content

Commit 159b668

Browse files
committed
feat - add unique check for tool names on run
1 parent adeb3b3 commit 159b668

File tree

2 files changed

+55
-5
lines changed

2 files changed

+55
-5
lines changed

pydantic_ai_slim/pydantic_ai/_agent_graph.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,13 @@ async def add_tool(tool: Tool[DepsT]) -> None:
222222
async def add_mcp_server_tools(server: MCPServer) -> None:
223223
if not server.is_running:
224224
raise exceptions.UserError(f'MCP server is not running: {server}')
225-
tool_defs = await server.list_tools()
226-
# TODO(Marcelo): We should check if the tool names are unique. If not, we should raise an error.
225+
tool_defs: list[ToolDefinition] = await server.list_tools()
226+
227+
tool_names: list[str] = [tool_def.name for tool_def in tool_defs]
228+
229+
if len(set(tool_names)) != len(tool_names):
230+
raise exceptions.UserError(f'Tool names must be unique: {str(tool_names)}')
231+
227232
function_tool_defs.extend(tool_defs)
228233

229234
await asyncio.gather(

tests/test_mcp.py

+48-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
"""Tests for the MCP (Model Context Protocol) server implementation."""
22

3+
import asyncio
4+
from collections.abc import AsyncIterator
5+
6+
import mcp.types as types
37
import pytest
8+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
49
from dirty_equals import IsInstance
510
from inline_snapshot import snapshot
611

@@ -11,13 +16,12 @@
1116
from .conftest import IsDatetime, try_import
1217

1318
with try_import() as imports_successful:
14-
from mcp.types import CallToolResult, TextContent
19+
from mcp.types import CallToolResult, JSONRPCMessage, TextContent
1520

16-
from pydantic_ai.mcp import MCPServerHTTP, MCPServerStdio
21+
from pydantic_ai.mcp import MCPServer, MCPServerHTTP, MCPServerStdio
1722
from pydantic_ai.models.openai import OpenAIModel
1823
from pydantic_ai.providers.openai import OpenAIProvider
1924

20-
2125
pytestmark = [
2226
pytest.mark.skipif(not imports_successful(), reason='mcp and openai not installed'),
2327
pytest.mark.anyio,
@@ -98,6 +102,47 @@ async def test_agent_with_stdio_server(allow_model_requests: None, openai_api_ke
98102
)
99103

100104

105+
async def test_server_with_duplicate_tool_names(allow_model_requests: None, openai_api_key: str):
106+
class MockClient:
107+
async def list_tools(self) -> types.ListToolsResult:
108+
# Await the sleep coroutine to simulate an asynchronous operation
109+
await asyncio.sleep(0)
110+
return types.ListToolsResult(
111+
tools=[
112+
types.Tool(name='x', description='', inputSchema={}),
113+
types.Tool(name='x', description='', inputSchema={}),
114+
]
115+
)
116+
117+
class MockMCPServer(MCPServer):
118+
async def client_streams(
119+
self,
120+
) -> AsyncIterator[
121+
tuple[MemoryObjectReceiveStream[JSONRPCMessage | Exception], MemoryObjectSendStream[JSONRPCMessage]]
122+
]:
123+
pass
124+
125+
is_running = True
126+
127+
async def __aenter__(self) -> 'MockMCPServer':
128+
return self
129+
130+
async def __aexit__(self, *args) -> None:
131+
pass
132+
133+
_client = MockClient()
134+
135+
server = MockMCPServer()
136+
model = OpenAIModel('gpt-4o', provider=OpenAIProvider(api_key=openai_api_key))
137+
agent = Agent(model, mcp_servers=[server])
138+
async with agent.run_mcp_servers():
139+
try:
140+
await agent.run('What is 0 degrees Celsius in Fahrenheit?')
141+
assert False
142+
except UserError as e:
143+
assert e.message == f'Tool names must be unique: {str(["x", "x"])}'
144+
145+
101146
async def test_agent_with_server_not_running(openai_api_key: str):
102147
server = MCPServerStdio('python', ['-m', 'tests.mcp_server'])
103148
model = OpenAIModel('gpt-4o', provider=OpenAIProvider(api_key=openai_api_key))

0 commit comments

Comments
 (0)