Skip to content
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
25 changes: 25 additions & 0 deletions langchain_mcp_adapters/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def __init__(
*,
callbacks: Callbacks | None = None,
tool_interceptors: list[ToolCallInterceptor] | None = None,
prefix_tool_name_with_server_name: bool = False,
) -> None:
"""Initialize a `MultiServerMCPClient` with MCP servers connections.

Expand All @@ -63,6 +64,10 @@ def __init__(
callbacks: Optional callbacks for handling notifications and events.
tool_interceptors: Optional list of tool call interceptors for modifying
requests and responses.
prefix_tool_name_with_server_name: Whether to prefix tool names with server
name to avoid collisions. When True, tool names will be formatted as
"{server_name}__{tool_name}". This is useful when using multiple
servers that may have tools with the same name.

!!! example "Basic usage (starting a new session on each tool call)"

Expand Down Expand Up @@ -98,12 +103,30 @@ def __init__(
async with client.session("math") as session:
tools = await load_mcp_tools(session)
```

!!! example "Using prefix to avoid tool name collisions"

```python
from langchain_mcp_adapters.client import MultiServerMCPClient

# When using multiple servers that may have tools with the same name
client = MultiServerMCPClient(
{
"server1": {...},
"server2": {...}
},
prefix_tool_name_with_server_name=True
)
# Tools will be named "server1__tool_name" and "server2__tool_name"
all_tools = await client.get_tools()
```
"""
self.connections: dict[str, Connection] = (
connections if connections is not None else {}
)
self.callbacks = callbacks or Callbacks()
self.tool_interceptors = tool_interceptors or []
self.prefix_tool_name_with_server_name = prefix_tool_name_with_server_name

@asynccontextmanager
async def session(
Expand Down Expand Up @@ -171,6 +194,7 @@ async def get_tools(self, *, server_name: str | None = None) -> list[BaseTool]:
callbacks=self.callbacks,
server_name=server_name,
tool_interceptors=self.tool_interceptors,
prefix_tool_name_with_server_name=self.prefix_tool_name_with_server_name,
)

all_tools: list[BaseTool] = []
Expand All @@ -183,6 +207,7 @@ async def get_tools(self, *, server_name: str | None = None) -> list[BaseTool]:
callbacks=self.callbacks,
server_name=name,
tool_interceptors=self.tool_interceptors,
prefix_tool_name_with_server_name=self.prefix_tool_name_with_server_name,
)
)
load_mcp_tool_tasks.append(load_mcp_tool_task)
Expand Down
16 changes: 15 additions & 1 deletion langchain_mcp_adapters/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ def convert_mcp_tool_to_langchain_tool(
callbacks: Callbacks | None = None,
tool_interceptors: list[ToolCallInterceptor] | None = None,
server_name: str | None = None,
prefix_tool_name_with_server_name: bool = False,
) -> BaseTool:
"""Convert an MCP tool to a LangChain tool.

Expand All @@ -174,6 +175,9 @@ def convert_mcp_tool_to_langchain_tool(
callbacks: Optional callbacks for handling notifications and events
tool_interceptors: Optional list of interceptors for tool call processing
server_name: Name of the server this tool belongs to
prefix_tool_name_with_server_name: Whether to prefix tool names with server
name to avoid collisions. When True, tool names will be formatted as
"{server_name}__{tool_name}".

Returns:
a LangChain tool
Expand Down Expand Up @@ -296,8 +300,13 @@ async def execute_tool(request: MCPToolCallRequest) -> MCPToolCallResult:
meta = {"_meta": meta} if meta is not None else {}
metadata = {**base, **meta} or None

# Prefix tool name with server name if requested
tool_name = tool.name
if prefix_tool_name_with_server_name and server_name:
tool_name = f"{server_name}__{tool.name}"

return StructuredTool(
name=tool.name,
name=tool_name,
description=tool.description or "",
args_schema=tool.inputSchema,
coroutine=call_tool,
Expand All @@ -313,6 +322,7 @@ async def load_mcp_tools(
callbacks: Callbacks | None = None,
tool_interceptors: list[ToolCallInterceptor] | None = None,
server_name: str | None = None,
prefix_tool_name_with_server_name: bool = False,
) -> list[BaseTool]:
"""Load all available MCP tools and convert them to LangChain [tools](https://docs.langchain.com/oss/python/langchain/tools).

Expand All @@ -322,6 +332,9 @@ async def load_mcp_tools(
callbacks: Optional `Callbacks` for handling notifications and events.
tool_interceptors: Optional list of interceptors for tool call processing.
server_name: Name of the server these tools belong to.
prefix_tool_name_with_server_name: Whether to prefix tool names with server
name to avoid collisions. When True, tool names will be formatted as
"{server_name}__{tool_name}".

Returns:
List of LangChain [tools](https://docs.langchain.com/oss/python/langchain/tools).
Expand Down Expand Up @@ -361,6 +374,7 @@ async def load_mcp_tools(
callbacks=callbacks,
tool_interceptors=tool_interceptors,
server_name=server_name,
prefix_tool_name_with_server_name=prefix_tool_name_with_server_name,
)
for tool in tools
]
Expand Down
101 changes: 101 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,104 @@ async def test_get_prompt():
assert isinstance(messages[0], AIMessage)
assert "You are a helpful assistant" in messages[0].content
assert "math, addition, multiplication" in messages[0].content


async def test_prefix_tool_name_with_server_name(
socket_enabled,
websocket_server,
websocket_server_port: int,
):
"""Test that tool names can be prefixed with server name to avoid collisions."""
# Get the absolute path to the server scripts
current_dir = Path(__file__).parent
math_server_path = os.path.join(current_dir, "servers/math_server.py")
weather_server_path = os.path.join(current_dir, "servers/weather_server.py")

client = MultiServerMCPClient(
{
"math": {
"command": "python3",
"args": [math_server_path],
"transport": "stdio",
},
"weather": {
"command": "python3",
"args": [weather_server_path],
"transport": "stdio",
},
"time": {
"url": f"ws://127.0.0.1:{websocket_server_port}/ws",
"transport": "websocket",
},
},
prefix_tool_name_with_server_name=True,
)

# Get all tools with prefixed names
all_tools = await client.get_tools()

# Should have 4 tools (add, multiply, get_weather, get_time)
assert len(all_tools) == 4

# Verify tool names are prefixed with server name
tool_names = {tool.name for tool in all_tools}
assert tool_names == {
"math__add",
"math__multiply",
"weather__get_weather",
"time__get_time",
}

# Test that we can still call the prefixed tools
add_tool = next(tool for tool in all_tools if tool.name == "math__add")
result = await add_tool.ainvoke({"a": 2, "b": 3})
assert result == "5"

# Test multiply tool
multiply_tool = next(tool for tool in all_tools if tool.name == "math__multiply")
result = await multiply_tool.ainvoke({"a": 4, "b": 5})
assert result == "20"

# Test weather tool
weather_tool = next(
tool for tool in all_tools if tool.name == "weather__get_weather"
)
result = await weather_tool.ainvoke({"location": "London"})
assert result == "It's always sunny in London"

# Test time tool
time_tool = next(tool for tool in all_tools if tool.name == "time__get_time")
result = await time_tool.ainvoke({"args": ""})
assert result == "5:20:00 PM EST"

# Test getting tools from a specific server still has prefix
math_tools = await client.get_tools(server_name="math")
assert len(math_tools) == 2
math_tool_names = {tool.name for tool in math_tools}
assert math_tool_names == {"math__add", "math__multiply"}


async def test_prefix_tool_name_disabled_by_default(socket_enabled):
"""Test that prefix_tool_name_with_server_name is disabled by default."""
# Get the absolute path to the server scripts
current_dir = Path(__file__).parent
math_server_path = os.path.join(current_dir, "servers/math_server.py")

# Create client without prefix option (should default to False)
client = MultiServerMCPClient(
{
"math": {
"command": "python3",
"args": [math_server_path],
"transport": "stdio",
}
}
)

all_tools = await client.get_tools()

# Verify tool names are NOT prefixed
tool_names = {tool.name for tool in all_tools}
assert tool_names == {"add", "multiply"}
assert "math__add" not in tool_names
assert "math__multiply" not in tool_names