You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_mcp_tools.py 10 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import logging
  2. from unittest.mock import AsyncMock, MagicMock
  3. import pytest
  4. from autogen_core import CancellationToken
  5. from autogen_ext.tools.mcp import (
  6. SseMcpToolAdapter,
  7. SseServerParams,
  8. StdioMcpToolAdapter,
  9. StdioServerParams,
  10. )
  11. from json_schema_to_pydantic import create_model
  12. from mcp import ClientSession, Tool
  13. @pytest.fixture
  14. def sample_tool() -> Tool:
  15. return Tool(
  16. name="test_tool",
  17. description="A test tool",
  18. inputSchema={
  19. "type": "object",
  20. "properties": {"test_param": {"type": "string"}},
  21. "required": ["test_param"],
  22. },
  23. )
  24. @pytest.fixture
  25. def sample_server_params() -> StdioServerParams:
  26. return StdioServerParams(command="echo", args=["test"])
  27. @pytest.fixture
  28. def sample_sse_tool() -> Tool:
  29. return Tool(
  30. name="test_sse_tool",
  31. description="A test SSE tool",
  32. inputSchema={
  33. "type": "object",
  34. "properties": {"test_param": {"type": "string"}},
  35. "required": ["test_param"],
  36. },
  37. )
  38. @pytest.fixture
  39. def mock_sse_session() -> AsyncMock:
  40. session = AsyncMock(spec=ClientSession)
  41. session.initialize = AsyncMock()
  42. session.call_tool = AsyncMock()
  43. session.list_tools = AsyncMock()
  44. return session
  45. @pytest.fixture
  46. def mock_session() -> AsyncMock:
  47. session = AsyncMock(spec=ClientSession)
  48. session.initialize = AsyncMock()
  49. session.call_tool = AsyncMock()
  50. session.list_tools = AsyncMock()
  51. return session
  52. @pytest.fixture
  53. def mock_tool_response() -> MagicMock:
  54. response = MagicMock()
  55. response.isError = False
  56. response.content = {"result": "test_output"}
  57. return response
  58. @pytest.fixture
  59. def cancellation_token() -> CancellationToken:
  60. return CancellationToken()
  61. def test_adapter_config_serialization(sample_tool: Tool, sample_server_params: StdioServerParams) -> None:
  62. """Test that adapter can be saved to and loaded from config."""
  63. original_adapter = StdioMcpToolAdapter(server_params=sample_server_params, tool=sample_tool)
  64. config = original_adapter.dump_component()
  65. loaded_adapter = StdioMcpToolAdapter.load_component(config)
  66. # Test that the loaded adapter has the same properties
  67. assert loaded_adapter.name == "test_tool"
  68. assert loaded_adapter.description == "A test tool"
  69. # Verify schema structure
  70. schema = loaded_adapter.schema
  71. assert "parameters" in schema, "Schema must have parameters"
  72. params_schema = schema["parameters"]
  73. assert isinstance(params_schema, dict), "Parameters must be a dict"
  74. assert "type" in params_schema, "Parameters must have type"
  75. assert "required" in params_schema, "Parameters must have required fields"
  76. assert "properties" in params_schema, "Parameters must have properties"
  77. # Compare schema content
  78. assert params_schema["type"] == sample_tool.inputSchema["type"]
  79. assert params_schema["required"] == sample_tool.inputSchema["required"]
  80. assert (
  81. params_schema["properties"]["test_param"]["type"] == sample_tool.inputSchema["properties"]["test_param"]["type"]
  82. )
  83. @pytest.mark.asyncio
  84. async def test_mcp_tool_execution(
  85. sample_tool: Tool,
  86. sample_server_params: StdioServerParams,
  87. mock_session: AsyncMock,
  88. mock_tool_response: MagicMock,
  89. cancellation_token: CancellationToken,
  90. monkeypatch: pytest.MonkeyPatch,
  91. caplog: pytest.LogCaptureFixture,
  92. ) -> None:
  93. """Test that adapter properly executes tools through ClientSession."""
  94. mock_context = AsyncMock()
  95. mock_context.__aenter__.return_value = mock_session
  96. monkeypatch.setattr(
  97. "autogen_ext.tools.mcp._base.create_mcp_server_session",
  98. lambda *args, **kwargs: mock_context, # type: ignore
  99. )
  100. mock_session.call_tool.return_value = mock_tool_response
  101. with caplog.at_level(logging.INFO):
  102. adapter = StdioMcpToolAdapter(server_params=sample_server_params, tool=sample_tool)
  103. result = await adapter.run_json(
  104. args=create_model(sample_tool.inputSchema)(**{"test_param": "test"}).model_dump(),
  105. cancellation_token=cancellation_token,
  106. )
  107. assert result == mock_tool_response.content
  108. mock_session.initialize.assert_called_once()
  109. mock_session.call_tool.assert_called_once()
  110. # Check log.
  111. assert "test_output" in caplog.text
  112. @pytest.mark.asyncio
  113. async def test_adapter_from_server_params(
  114. sample_tool: Tool,
  115. sample_server_params: StdioServerParams,
  116. mock_session: AsyncMock,
  117. monkeypatch: pytest.MonkeyPatch,
  118. ) -> None:
  119. """Test that adapter can be created from server parameters."""
  120. mock_context = AsyncMock()
  121. mock_context.__aenter__.return_value = mock_session
  122. monkeypatch.setattr(
  123. "autogen_ext.tools.mcp._base.create_mcp_server_session",
  124. lambda *args, **kwargs: mock_context, # type: ignore
  125. )
  126. mock_session.list_tools.return_value.tools = [sample_tool]
  127. adapter = await StdioMcpToolAdapter.from_server_params(sample_server_params, "test_tool")
  128. assert isinstance(adapter, StdioMcpToolAdapter)
  129. assert adapter.name == "test_tool"
  130. assert adapter.description == "A test tool"
  131. # Verify schema structure
  132. schema = adapter.schema
  133. assert "parameters" in schema, "Schema must have parameters"
  134. params_schema = schema["parameters"]
  135. assert isinstance(params_schema, dict), "Parameters must be a dict"
  136. assert "type" in params_schema, "Parameters must have type"
  137. assert "required" in params_schema, "Parameters must have required fields"
  138. assert "properties" in params_schema, "Parameters must have properties"
  139. # Compare schema content
  140. assert params_schema["type"] == sample_tool.inputSchema["type"]
  141. assert params_schema["required"] == sample_tool.inputSchema["required"]
  142. assert (
  143. params_schema["properties"]["test_param"]["type"] == sample_tool.inputSchema["properties"]["test_param"]["type"]
  144. )
  145. @pytest.mark.asyncio
  146. async def test_sse_adapter_config_serialization(sample_sse_tool: Tool) -> None:
  147. """Test that SSE adapter can be saved to and loaded from config."""
  148. params = SseServerParams(url="http://test-url")
  149. original_adapter = SseMcpToolAdapter(server_params=params, tool=sample_sse_tool)
  150. config = original_adapter.dump_component()
  151. loaded_adapter = SseMcpToolAdapter.load_component(config)
  152. # Test that the loaded adapter has the same properties
  153. assert loaded_adapter.name == "test_sse_tool"
  154. assert loaded_adapter.description == "A test SSE tool"
  155. # Verify schema structure
  156. schema = loaded_adapter.schema
  157. assert "parameters" in schema, "Schema must have parameters"
  158. params_schema = schema["parameters"]
  159. assert isinstance(params_schema, dict), "Parameters must be a dict"
  160. assert "type" in params_schema, "Parameters must have type"
  161. assert "required" in params_schema, "Parameters must have required fields"
  162. assert "properties" in params_schema, "Parameters must have properties"
  163. # Compare schema content
  164. assert params_schema["type"] == sample_sse_tool.inputSchema["type"]
  165. assert params_schema["required"] == sample_sse_tool.inputSchema["required"]
  166. assert (
  167. params_schema["properties"]["test_param"]["type"]
  168. == sample_sse_tool.inputSchema["properties"]["test_param"]["type"]
  169. )
  170. @pytest.mark.asyncio
  171. async def test_sse_tool_execution(
  172. sample_sse_tool: Tool,
  173. mock_sse_session: AsyncMock,
  174. monkeypatch: pytest.MonkeyPatch,
  175. caplog: pytest.LogCaptureFixture,
  176. ) -> None:
  177. """Test that SSE adapter properly executes tools through ClientSession."""
  178. params = SseServerParams(url="http://test-url")
  179. mock_context = AsyncMock()
  180. mock_context.__aenter__.return_value = mock_sse_session
  181. mock_sse_session.call_tool.return_value = MagicMock(isError=False, content={"result": "test_output"})
  182. monkeypatch.setattr(
  183. "autogen_ext.tools.mcp._base.create_mcp_server_session",
  184. lambda *args, **kwargs: mock_context, # type: ignore
  185. )
  186. with caplog.at_level(logging.INFO):
  187. adapter = SseMcpToolAdapter(server_params=params, tool=sample_sse_tool)
  188. result = await adapter.run_json(
  189. args=create_model(sample_sse_tool.inputSchema)(**{"test_param": "test"}).model_dump(),
  190. cancellation_token=CancellationToken(),
  191. )
  192. assert result == mock_sse_session.call_tool.return_value.content
  193. mock_sse_session.initialize.assert_called_once()
  194. mock_sse_session.call_tool.assert_called_once()
  195. # Check log.
  196. assert "test_output" in caplog.text
  197. @pytest.mark.asyncio
  198. async def test_sse_adapter_from_server_params(
  199. sample_sse_tool: Tool,
  200. mock_sse_session: AsyncMock,
  201. monkeypatch: pytest.MonkeyPatch,
  202. ) -> None:
  203. """Test that SSE adapter can be created from server parameters."""
  204. params = SseServerParams(url="http://test-url")
  205. mock_context = AsyncMock()
  206. mock_context.__aenter__.return_value = mock_sse_session
  207. monkeypatch.setattr(
  208. "autogen_ext.tools.mcp._base.create_mcp_server_session",
  209. lambda *args, **kwargs: mock_context, # type: ignore
  210. )
  211. mock_sse_session.list_tools.return_value.tools = [sample_sse_tool]
  212. adapter = await SseMcpToolAdapter.from_server_params(params, "test_sse_tool")
  213. assert isinstance(adapter, SseMcpToolAdapter)
  214. assert adapter.name == "test_sse_tool"
  215. assert adapter.description == "A test SSE tool"
  216. # Verify schema structure
  217. schema = adapter.schema
  218. assert "parameters" in schema, "Schema must have parameters"
  219. params_schema = schema["parameters"]
  220. assert isinstance(params_schema, dict), "Parameters must be a dict"
  221. assert "type" in params_schema, "Parameters must have type"
  222. assert "required" in params_schema, "Parameters must have required fields"
  223. assert "properties" in params_schema, "Parameters must have properties"
  224. # Compare schema content
  225. assert params_schema["type"] == sample_sse_tool.inputSchema["type"]
  226. assert params_schema["required"] == sample_sse_tool.inputSchema["required"]
  227. assert (
  228. params_schema["properties"]["test_param"]["type"]
  229. == sample_sse_tool.inputSchema["properties"]["test_param"]["type"]
  230. )