@@ -1732,202 +1732,6 @@ async def test_include_prompts_requires_pii(
17321732 assert "gen_ai.response.text" not in span ["data" ]
17331733
17341734
1735- @pytest .mark .parametrize ("stream_gen_ai_spans" , [True , False ])
1736- @pytest .mark .asyncio
1737- async def test_mcp_tool_execution_spans (
1738- sentry_init ,
1739- capture_events ,
1740- capture_items ,
1741- stream_gen_ai_spans ,
1742- ):
1743- """
1744- Test that MCP (Model Context Protocol) tool calls create execute_tool spans.
1745-
1746- Tests MCP tools accessed through CombinedToolset, which is how they're typically
1747- used in practice (when an agent combines regular functions with MCP servers).
1748- """
1749- pytest .importorskip ("mcp" )
1750-
1751- from unittest .mock import MagicMock
1752-
1753- from pydantic_ai import Agent
1754- from pydantic_ai .mcp import MCPServerStdio
1755- from pydantic_ai .toolsets .combined import CombinedToolset
1756-
1757- import sentry_sdk
1758-
1759- # Create mock MCP server
1760- mock_server = MCPServerStdio (
1761- command = "python" ,
1762- args = ["-m" , "test_server" ],
1763- )
1764-
1765- # Mock the server's internal methods
1766- mock_server ._client = MagicMock ()
1767- mock_server ._is_initialized = True
1768- mock_server ._server_info = MagicMock ()
1769-
1770- # Mock tool call response
1771- async def mock_send_request (request , response_type ):
1772- from mcp .types import CallToolResult , TextContent
1773-
1774- return CallToolResult (
1775- content = [TextContent (type = "text" , text = "MCP tool executed successfully" )],
1776- isError = False ,
1777- )
1778-
1779- mock_server ._client .send_request = mock_send_request
1780-
1781- # Mock context manager methods
1782- async def mock_aenter ():
1783- return mock_server
1784-
1785- async def mock_aexit (* args ):
1786- pass
1787-
1788- mock_server .__aenter__ = mock_aenter
1789- mock_server .__aexit__ = mock_aexit
1790-
1791- # Mock _map_tool_result_part
1792- async def mock_map_tool_result_part (part ):
1793- return part .text if hasattr (part , "text" ) else str (part )
1794-
1795- mock_server ._map_tool_result_part = mock_map_tool_result_part
1796-
1797- # Create a CombinedToolset with the MCP server
1798- # This simulates how MCP servers are typically used in practice
1799- from pydantic_ai .toolsets .function import FunctionToolset
1800-
1801- function_toolset = FunctionToolset ()
1802- combined = CombinedToolset ([function_toolset , mock_server ])
1803-
1804- # Create agent
1805- agent = Agent (
1806- "test" ,
1807- name = "test_mcp_agent" ,
1808- )
1809-
1810- sentry_init (
1811- integrations = [PydanticAIIntegration ()],
1812- traces_sample_rate = 1.0 ,
1813- send_default_pii = True ,
1814- stream_gen_ai_spans = stream_gen_ai_spans ,
1815- )
1816-
1817- if stream_gen_ai_spans :
1818- items = capture_items ("transaction" , "span" )
1819-
1820- # Simulate MCP tool execution within a transaction through CombinedToolset
1821- with sentry_sdk .start_transaction (
1822- op = "ai.run" , name = "invoke_agent test_mcp_agent"
1823- ):
1824- # Set up the agent context
1825- scope = sentry_sdk .get_current_scope ()
1826- scope ._contexts ["pydantic_ai_agent" ] = {
1827- "_agent" : agent ,
1828- }
1829-
1830- # Create a mock tool that simulates an MCP tool from CombinedToolset
1831- from pydantic_ai ._run_context import RunContext
1832- from pydantic_ai .models .test import TestModel
1833- from pydantic_ai .result import RunUsage
1834- from pydantic_ai .toolsets .combined import _CombinedToolsetTool
1835-
1836- ctx = RunContext (
1837- deps = None ,
1838- model = TestModel (),
1839- usage = RunUsage (),
1840- retry = 0 ,
1841- tool_name = "test_mcp_tool" ,
1842- )
1843-
1844- tool_name = "test_mcp_tool"
1845-
1846- # Create a tool that points to the MCP server
1847- # This simulates how CombinedToolset wraps tools from different sources
1848- tool = _CombinedToolsetTool (
1849- toolset = combined ,
1850- tool_def = MagicMock (name = tool_name ),
1851- max_retries = 0 ,
1852- args_validator = MagicMock (),
1853- source_toolset = mock_server ,
1854- source_tool = MagicMock (),
1855- )
1856-
1857- try :
1858- await combined .call_tool (tool_name , {"query" : "test" }, ctx , tool )
1859- except Exception :
1860- # MCP tool might raise if not fully mocked, that's okay
1861- pass
1862-
1863- if len (items ) == 0 :
1864- pytest .skip ("No events captured, MCP test setup incomplete" )
1865-
1866- (transaction ,) = (item .payload for item in items if item .type == "transaction" )
1867- transaction ["spans" ]
1868- else :
1869- events = capture_events ()
1870-
1871- # Simulate MCP tool execution within a transaction through CombinedToolset
1872- with sentry_sdk .start_transaction (
1873- op = "ai.run" , name = "invoke_agent test_mcp_agent"
1874- ) as transaction :
1875- # Set up the agent context
1876- scope = sentry_sdk .get_current_scope ()
1877- scope ._contexts ["pydantic_ai_agent" ] = {
1878- "_agent" : agent ,
1879- }
1880-
1881- # Create a mock tool that simulates an MCP tool from CombinedToolset
1882- from pydantic_ai ._run_context import RunContext
1883- from pydantic_ai .models .test import TestModel
1884- from pydantic_ai .result import RunUsage
1885- from pydantic_ai .toolsets .combined import _CombinedToolsetTool
1886-
1887- ctx = RunContext (
1888- deps = None ,
1889- model = TestModel (),
1890- usage = RunUsage (),
1891- retry = 0 ,
1892- tool_name = "test_mcp_tool" ,
1893- )
1894-
1895- tool_name = "test_mcp_tool"
1896-
1897- # Create a tool that points to the MCP server
1898- # This simulates how CombinedToolset wraps tools from different sources
1899- tool = _CombinedToolsetTool (
1900- toolset = combined ,
1901- tool_def = MagicMock (name = tool_name ),
1902- max_retries = 0 ,
1903- args_validator = MagicMock (),
1904- source_toolset = mock_server ,
1905- source_tool = MagicMock (),
1906- )
1907-
1908- try :
1909- await combined .call_tool (tool_name , {"query" : "test" }, ctx , tool )
1910- except Exception :
1911- # MCP tool might raise if not fully mocked, that's okay
1912- pass
1913-
1914- events_list = events
1915- if len (events_list ) == 0 :
1916- pytest .skip ("No events captured, MCP test setup incomplete" )
1917-
1918- (transaction ,) = events_list
1919- transaction ["spans" ]
1920-
1921- # Note: This test manually calls combined.call_tool which doesn't go through
1922- # ToolManager._call_tool (which is what the integration patches).
1923- # In real-world usage, MCP tools are called through agent.run() which uses ToolManager.
1924- # This synthetic test setup doesn't trigger the integration's tool patches.
1925- # We skip this test as it doesn't represent actual usage patterns.
1926- pytest .skip (
1927- "MCP test needs to be rewritten to use agent.run() instead of manually calling toolset methods"
1928- )
1929-
1930-
19311735@pytest .mark .asyncio
19321736async def test_context_cleanup_after_run (sentry_init , get_test_agent ):
19331737 """
0 commit comments