Skip to content

Commit 072bf41

Browse files
authored
add more tests (#69)
1 parent 2178e7e commit 072bf41

File tree

5 files changed

+370
-0
lines changed

5 files changed

+370
-0
lines changed

tests/servers/__init__.py

Whitespace-only changes.

tests/servers/math_server.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from mcp.server.fastmcp import FastMCP
2+
3+
mcp = FastMCP("Math")
4+
5+
6+
@mcp.tool()
7+
def add(a: int, b: int) -> int:
8+
"""Add two numbers"""
9+
return a + b
10+
11+
12+
@mcp.tool()
13+
def multiply(a: int, b: int) -> int:
14+
"""Multiply two numbers"""
15+
return a * b
16+
17+
18+
@mcp.prompt()
19+
def configure_assistant(skills: str) -> str:
20+
return [
21+
{
22+
"role": "assistant",
23+
"content": f"You are a helpful assistant. You have the following skills: {skills}. Always use only one tool at a time.",
24+
}
25+
]
26+
27+
28+
if __name__ == "__main__":
29+
mcp.run(transport="stdio")

tests/servers/weather_server.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from mcp.server.fastmcp import FastMCP
2+
3+
mcp = FastMCP("Weather")
4+
5+
6+
@mcp.tool()
7+
async def get_weather(location: str) -> str:
8+
"""Get weather for location."""
9+
return f"It's always sunny in {location}"
10+
11+
12+
if __name__ == "__main__":
13+
mcp.run(transport="stdio")

tests/test_client.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import os
2+
from pathlib import Path
3+
4+
import pytest
5+
from langchain_core.messages import AIMessage
6+
from langchain_core.tools import BaseTool
7+
8+
from langchain_mcp_adapters.client import MultiServerMCPClient
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_multi_server_mcp_client():
13+
"""Test that the MultiServerMCPClient can connect to multiple servers and load tools."""
14+
15+
# Get the absolute path to the server scripts
16+
current_dir = Path(__file__).parent
17+
math_server_path = os.path.join(current_dir, "servers/math_server.py")
18+
weather_server_path = os.path.join(current_dir, "servers/weather_server.py")
19+
20+
async with MultiServerMCPClient(
21+
{
22+
"math": {
23+
"command": "python",
24+
"args": [math_server_path],
25+
"transport": "stdio",
26+
},
27+
"weather": {
28+
"command": "python",
29+
"args": [weather_server_path],
30+
"transport": "stdio",
31+
},
32+
}
33+
) as client:
34+
# Check that we have tools from both servers
35+
all_tools = client.get_tools()
36+
37+
# Should have 3 tools (add, multiply, get_weather)
38+
assert len(all_tools) == 3
39+
40+
# Check that tools are BaseTool instances
41+
for tool in all_tools:
42+
assert isinstance(tool, BaseTool)
43+
44+
# Verify tool names
45+
tool_names = {tool.name for tool in all_tools}
46+
assert tool_names == {"add", "multiply", "get_weather"}
47+
48+
# Check math server tools
49+
math_tools = client.server_name_to_tools["math"]
50+
assert len(math_tools) == 2
51+
math_tool_names = {tool.name for tool in math_tools}
52+
assert math_tool_names == {"add", "multiply"}
53+
54+
# Check weather server tools
55+
weather_tools = client.server_name_to_tools["weather"]
56+
assert len(weather_tools) == 1
57+
assert weather_tools[0].name == "get_weather"
58+
59+
# Test that we can call a math tool
60+
add_tool = next(tool for tool in all_tools if tool.name == "add")
61+
result = await add_tool.ainvoke({"a": 2, "b": 3})
62+
assert result == "5"
63+
64+
# Test that we can call a weather tool
65+
weather_tool = next(tool for tool in all_tools if tool.name == "get_weather")
66+
result = await weather_tool.ainvoke({"location": "London"})
67+
assert result == "It's always sunny in London"
68+
69+
# Test the multiply tool
70+
multiply_tool = next(tool for tool in all_tools if tool.name == "multiply")
71+
result = await multiply_tool.ainvoke({"a": 4, "b": 5})
72+
assert result == "20"
73+
74+
75+
@pytest.mark.asyncio
76+
async def test_multi_server_connect_methods():
77+
"""Test the different connect methods for MultiServerMCPClient."""
78+
79+
# Get the absolute path to the server scripts
80+
current_dir = Path(__file__).parent
81+
math_server_path = os.path.join(current_dir, "servers/math_server.py")
82+
weather_server_path = os.path.join(current_dir, "servers/weather_server.py")
83+
84+
# Initialize client without initial connections
85+
client = MultiServerMCPClient()
86+
async with client:
87+
# Connect to math server using connect_to_server
88+
await client.connect_to_server(
89+
"math", transport="stdio", command="python", args=[math_server_path]
90+
)
91+
92+
# Connect to weather server using connect_to_server_via_stdio
93+
await client.connect_to_server_via_stdio(
94+
"weather", command="python", args=[weather_server_path]
95+
)
96+
97+
# Check that we have tools from both servers
98+
all_tools = client.get_tools()
99+
assert len(all_tools) == 3
100+
101+
# Verify tool names
102+
tool_names = {tool.name for tool in all_tools}
103+
assert tool_names == {"add", "multiply", "get_weather"}
104+
105+
106+
@pytest.mark.asyncio
107+
async def test_get_prompt():
108+
"""Test retrieving prompts from MCP servers."""
109+
110+
# Get the absolute path to the server scripts
111+
current_dir = Path(__file__).parent
112+
math_server_path = os.path.join(current_dir, "servers/math_server.py")
113+
114+
async with MultiServerMCPClient(
115+
{
116+
"math": {
117+
"command": "python",
118+
"args": [math_server_path],
119+
"transport": "stdio",
120+
}
121+
}
122+
) as client:
123+
# Test getting a prompt from the math server
124+
messages = await client.get_prompt(
125+
"math", "configure_assistant", {"skills": "math, addition, multiplication"}
126+
)
127+
128+
# Check that we got an AIMessage back
129+
assert len(messages) == 1
130+
assert isinstance(messages[0], AIMessage)
131+
assert "You are a helpful assistant" in messages[0].content
132+
assert "math, addition, multiplication" in messages[0].content

tests/test_tools.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from unittest.mock import AsyncMock, MagicMock
2+
3+
import pytest
4+
from langchain_core.messages import ToolMessage
5+
from langchain_core.tools import BaseTool, ToolException
6+
from mcp.types import (
7+
CallToolResult,
8+
EmbeddedResource,
9+
ImageContent,
10+
TextContent,
11+
TextResourceContents,
12+
)
13+
from mcp.types import Tool as MCPTool
14+
15+
from langchain_mcp_adapters.tools import (
16+
_convert_call_tool_result,
17+
convert_mcp_tool_to_langchain_tool,
18+
load_mcp_tools,
19+
)
20+
21+
22+
def test_convert_single_text_content():
23+
# Test with a single text content
24+
result = CallToolResult(
25+
content=[TextContent(type="text", text="test result")],
26+
isError=False,
27+
)
28+
29+
text_content, non_text_content = _convert_call_tool_result(result)
30+
31+
assert text_content == "test result"
32+
assert non_text_content is None
33+
34+
35+
def test_convert_multiple_text_contents():
36+
# Test with multiple text contents
37+
result = CallToolResult(
38+
content=[
39+
TextContent(type="text", text="result 1"),
40+
TextContent(type="text", text="result 2"),
41+
],
42+
isError=False,
43+
)
44+
45+
text_content, non_text_content = _convert_call_tool_result(result)
46+
47+
assert text_content == ["result 1", "result 2"]
48+
assert non_text_content is None
49+
50+
51+
def test_convert_with_non_text_content():
52+
# Test with non-text content
53+
image_content = ImageContent(type="image", mimeType="image/png", data="base64data")
54+
resource_content = EmbeddedResource(
55+
type="resource",
56+
resource=TextResourceContents(uri="resource://test", mimeType="text/plain", text="hi"),
57+
)
58+
59+
result = CallToolResult(
60+
content=[
61+
TextContent(type="text", text="text result"),
62+
image_content,
63+
resource_content,
64+
],
65+
isError=False,
66+
)
67+
68+
text_content, non_text_content = _convert_call_tool_result(result)
69+
70+
assert text_content == "text result"
71+
assert non_text_content == [image_content, resource_content]
72+
73+
74+
def test_convert_with_error():
75+
# Test with error
76+
result = CallToolResult(
77+
content=[TextContent(type="text", text="error message")],
78+
isError=True,
79+
)
80+
81+
with pytest.raises(ToolException) as exc_info:
82+
_convert_call_tool_result(result)
83+
84+
assert str(exc_info.value) == "error message"
85+
86+
87+
@pytest.mark.asyncio
88+
async def test_convert_mcp_tool_to_langchain_tool():
89+
tool_input_schema = {
90+
"properties": {
91+
"param1": {"title": "Param1", "type": "string"},
92+
"param2": {"title": "Param2", "type": "integer"},
93+
},
94+
"required": ["param1", "param2"],
95+
"title": "ToolSchema",
96+
"type": "object",
97+
}
98+
# Mock session and MCP tool
99+
session = AsyncMock()
100+
session.call_tool.return_value = CallToolResult(
101+
content=[TextContent(type="text", text="tool result")],
102+
isError=False,
103+
)
104+
105+
mcp_tool = MCPTool(
106+
name="test_tool",
107+
description="Test tool description",
108+
inputSchema=tool_input_schema,
109+
)
110+
111+
# Convert MCP tool to LangChain tool
112+
lc_tool = convert_mcp_tool_to_langchain_tool(session, mcp_tool)
113+
114+
# Verify the converted tool
115+
assert lc_tool.name == "test_tool"
116+
assert lc_tool.description == "Test tool description"
117+
assert lc_tool.args_schema == tool_input_schema
118+
119+
# Test calling the tool
120+
result = await lc_tool.ainvoke(
121+
{"args": {"param1": "test", "param2": 42}, "id": "1", "type": "tool_call"}
122+
)
123+
124+
# Verify session.call_tool was called with correct arguments
125+
session.call_tool.assert_called_once_with("test_tool", {"param1": "test", "param2": 42})
126+
127+
# Verify result
128+
assert result == ToolMessage(content="tool result", name="test_tool", tool_call_id="1")
129+
130+
131+
@pytest.mark.asyncio
132+
async def test_load_mcp_tools():
133+
tool_input_schema = {
134+
"properties": {
135+
"param1": {"title": "Param1", "type": "string"},
136+
"param2": {"title": "Param2", "type": "integer"},
137+
},
138+
"required": ["param1", "param2"],
139+
"title": "ToolSchema",
140+
"type": "object",
141+
}
142+
# Mock session and list_tools response
143+
session = AsyncMock()
144+
mcp_tools = [
145+
MCPTool(
146+
name="tool1",
147+
description="Tool 1 description",
148+
inputSchema=tool_input_schema,
149+
),
150+
MCPTool(
151+
name="tool2",
152+
description="Tool 2 description",
153+
inputSchema=tool_input_schema,
154+
),
155+
]
156+
session.list_tools.return_value = MagicMock(tools=mcp_tools)
157+
158+
# Mock call_tool to return different results for different tools
159+
async def mock_call_tool(tool_name, arguments):
160+
if tool_name == "tool1":
161+
return CallToolResult(
162+
content=[TextContent(type="text", text=f"tool1 result with {arguments}")],
163+
isError=False,
164+
)
165+
else:
166+
return CallToolResult(
167+
content=[TextContent(type="text", text=f"tool2 result with {arguments}")],
168+
isError=False,
169+
)
170+
171+
session.call_tool.side_effect = mock_call_tool
172+
173+
# Load MCP tools
174+
tools = await load_mcp_tools(session)
175+
176+
# Verify the tools
177+
assert len(tools) == 2
178+
assert all(isinstance(tool, BaseTool) for tool in tools)
179+
assert tools[0].name == "tool1"
180+
assert tools[1].name == "tool2"
181+
182+
# Test calling the first tool
183+
result1 = await tools[0].ainvoke(
184+
{"args": {"param1": "test1", "param2": 1}, "id": "1", "type": "tool_call"}
185+
)
186+
assert result1 == ToolMessage(
187+
content="tool1 result with {'param1': 'test1', 'param2': 1}", name="tool1", tool_call_id="1"
188+
)
189+
190+
# Test calling the second tool
191+
result2 = await tools[1].ainvoke(
192+
{"args": {"param1": "test2", "param2": 2}, "id": "2", "type": "tool_call"}
193+
)
194+
assert result2 == ToolMessage(
195+
content="tool2 result with {'param1': 'test2', 'param2': 2}", name="tool2", tool_call_id="2"
196+
)

0 commit comments

Comments
 (0)