@@ -992,3 +992,188 @@ class CustomAgentState(AgentState):
992992 isinstance (msg , ToolMessage ) and msg .content == "Counter updated!"
993993 for msg in result ["messages" ]
994994 )
995+
996+
997+ # Tests for tool_name_prefix functionality
998+
999+
1000+ def _create_weather_search_server ():
1001+ """Create a weather server with a search tool."""
1002+ server = FastMCP (port = 8185 )
1003+
1004+ @server .tool ()
1005+ def search (query : str ) -> str :
1006+ """Search for weather information"""
1007+ return f"Weather results for: { query } "
1008+
1009+ return server
1010+
1011+
1012+ def _create_flights_search_server ():
1013+ """Create a flights server with a search tool."""
1014+ server = FastMCP (port = 8186 )
1015+
1016+ @server .tool ()
1017+ def search (destination : str ) -> str :
1018+ """Search for flights"""
1019+ return f"Flight results to: { destination } "
1020+
1021+ return server
1022+
1023+
1024+ @pytest .mark .skipif (not LANGCHAIN_INSTALLED , reason = "langchain not installed" )
1025+ async def test_parallel_tool_invocation_across_multiple_servers (socket_enabled ) -> None :
1026+ """Test that two servers with identically named tools can be invoked in parallel.
1027+
1028+ This test verifies that:
1029+ 1. Two MCP servers can each expose a tool with the same name (search)
1030+ 2. With tool_name_prefix=True, they get unique LangChain names
1031+ (weather_search, flights_search)
1032+ 3. When an LLM calls both tools in parallel,
1033+ each tool is routed to the correct server
1034+ 4. The correct results come back from each server
1035+ """
1036+ from langchain .agents import AgentState , create_agent # noqa: PLC0415
1037+ from langgraph .checkpoint .memory import MemorySaver # noqa: PLC0415
1038+
1039+ with (
1040+ run_streamable_http (_create_weather_search_server , 8185 ),
1041+ run_streamable_http (_create_flights_search_server , 8186 ),
1042+ ):
1043+ client = MultiServerMCPClient (
1044+ {
1045+ "weather" : {
1046+ "url" : "http://localhost:8185/mcp" ,
1047+ "transport" : "streamable_http" ,
1048+ },
1049+ "flights" : {
1050+ "url" : "http://localhost:8186/mcp" ,
1051+ "transport" : "streamable_http" ,
1052+ },
1053+ },
1054+ tool_name_prefix = True ,
1055+ )
1056+ tools = await client .get_tools ()
1057+
1058+ # Verify we have both prefixed tools
1059+ assert len (tools ) == 2
1060+ tool_names = {t .name for t in tools }
1061+ assert tool_names == {"weather_search" , "flights_search" }
1062+
1063+ # Simulate an LLM calling both tools in parallel (common pattern for agents)
1064+ model = FixedGenericFakeChatModel (
1065+ messages = iter (
1066+ [
1067+ AIMessage (
1068+ content = "" ,
1069+ tool_calls = [
1070+ {
1071+ "name" : "weather_search" ,
1072+ "args" : {"query" : "sunny in Paris" },
1073+ "id" : "call_weather" ,
1074+ "type" : "tool_call" ,
1075+ },
1076+ {
1077+ "name" : "flights_search" ,
1078+ "args" : {"destination" : "Tokyo" },
1079+ "id" : "call_flights" ,
1080+ "type" : "tool_call" ,
1081+ },
1082+ ],
1083+ ),
1084+ AIMessage (content = "Here are your results." ),
1085+ ]
1086+ )
1087+ )
1088+
1089+ agent = create_agent (
1090+ model ,
1091+ tools ,
1092+ state_schema = AgentState ,
1093+ checkpointer = MemorySaver (),
1094+ )
1095+
1096+ # Run the agent - both tools should be called in parallel
1097+ result = await agent .ainvoke (
1098+ {"messages" : [HumanMessage (content = "Search weather and flights" )]},
1099+ {"configurable" : {"thread_id" : "test_parallel" }},
1100+ )
1101+
1102+ # Verify both tools were called and returned correct results
1103+ tool_messages = [
1104+ msg for msg in result ["messages" ] if isinstance (msg , ToolMessage )
1105+ ]
1106+ assert len (tool_messages ) == 2
1107+
1108+ # Create a mapping of tool_call_id to content for easier assertion
1109+ results_by_id = {msg .tool_call_id : msg .content for msg in tool_messages }
1110+
1111+ # Verify the weather search was routed to the weather server
1112+ assert results_by_id ["call_weather" ] == [
1113+ {
1114+ "type" : "text" ,
1115+ "text" : "Weather results for: sunny in Paris" ,
1116+ "id" : IsLangChainID ,
1117+ }
1118+ ]
1119+
1120+ # Verify the flights search was routed to the flights server
1121+ assert results_by_id ["call_flights" ] == [
1122+ {
1123+ "type" : "text" ,
1124+ "text" : "Flight results to: Tokyo" ,
1125+ "id" : IsLangChainID ,
1126+ }
1127+ ]
1128+
1129+
1130+ async def test_get_tools_with_name_conflict (socket_enabled ) -> None :
1131+ """Test fetching tools with name conflict using tool_name_prefix.
1132+
1133+ This test verifies that:
1134+ 1. Without tool_name_prefix, both servers would have conflicting "search" tool names
1135+ 2. With tool_name_prefix=True, tools get unique names
1136+ (weather_search, flights_search)
1137+ """
1138+ with (
1139+ run_streamable_http (_create_weather_search_server , 8185 ),
1140+ run_streamable_http (_create_flights_search_server , 8186 ),
1141+ ):
1142+ # First, verify that without prefix both tools would have the same name
1143+ client_no_prefix = MultiServerMCPClient (
1144+ {
1145+ "weather" : {
1146+ "url" : "http://localhost:8185/mcp" ,
1147+ "transport" : "streamable_http" ,
1148+ },
1149+ "flights" : {
1150+ "url" : "http://localhost:8186/mcp" ,
1151+ "transport" : "streamable_http" ,
1152+ },
1153+ },
1154+ tool_name_prefix = False ,
1155+ )
1156+ tools_no_prefix = await client_no_prefix .get_tools ()
1157+ # Both tools are named "search" without prefix
1158+ assert all (t .name == "search" for t in tools_no_prefix )
1159+
1160+ # Now test with prefix - tools should be disambiguated
1161+ client = MultiServerMCPClient (
1162+ {
1163+ "weather" : {
1164+ "url" : "http://localhost:8185/mcp" ,
1165+ "transport" : "streamable_http" ,
1166+ },
1167+ "flights" : {
1168+ "url" : "http://localhost:8186/mcp" ,
1169+ "transport" : "streamable_http" ,
1170+ },
1171+ },
1172+ tool_name_prefix = True ,
1173+ )
1174+ tools = await client .get_tools ()
1175+
1176+ # Verify we have both prefixed tools with unique names
1177+ assert len (tools ) == 2
1178+ tool_names = {t .name for t in tools }
1179+ assert tool_names == {"weather_search" , "flights_search" }
0 commit comments