diff --git a/sentry_sdk/integrations/mcp.py b/sentry_sdk/integrations/mcp.py index af3c5272c8..66a1df722a 100644 --- a/sentry_sdk/integrations/mcp.py +++ b/sentry_sdk/integrations/mcp.py @@ -53,16 +53,6 @@ class MCPIntegration(Integration): identifier = "mcp" origin = "auto.ai.mcp" - def __init__(self, include_prompts: bool = True) -> None: - """ - Initialize the MCP integration. - - Args: - include_prompts: Whether to include prompts (tool results and prompt content) - in span data. Requires send_default_pii=True. Default is True. - """ - self.include_prompts = include_prompts - @staticmethod def setup_once() -> None: """ @@ -307,7 +297,7 @@ async def _instrument_tool_call( if has_data_collection_enabled(client.options): if client.options["data_collection"]["gen_ai"]["outputs"]: should_include_result_data = True - elif should_send_default_pii() and integration.include_prompts: + elif should_send_default_pii(): should_include_result_data = True result_content = result @@ -399,7 +389,7 @@ async def _instrument_prompt_get( if has_data_collection_enabled(client.options): if client.options["data_collection"]["gen_ai"]["inputs"]: should_include_result_data = True - elif should_send_default_pii() and integration.include_prompts: + elif should_send_default_pii(): should_include_result_data = True # For prompts, count messages and set role/content only for single-message prompts diff --git a/tests/integrations/fastmcp/test_fastmcp.py b/tests/integrations/fastmcp/test_fastmcp.py index 08f6000926..10699de562 100644 --- a/tests/integrations/fastmcp/test_fastmcp.py +++ b/tests/integrations/fastmcp/test_fastmcp.py @@ -11,7 +11,7 @@ - Span creation when tools/prompts/resources are called through MCP protocol - Span data accuracy (operation, name, origin, etc.) - Error capture and instrumentation -- PII and include_prompts flag behavior +- PII flag behavior - Request context data extraction - Transport detection (stdio, http, sse) @@ -267,20 +267,19 @@ def reset_request_ctx(): @pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_fastmcp_tool_sync( sentry_init, capture_items, FastMCP, send_default_pii, - include_prompts, stdio, ): """Test that FastMCP synchronous tool handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -320,7 +319,7 @@ def add_numbers(a: int, b: int) -> dict: assert span["attributes"][SPANDATA.MCP_REQUEST_ID] == "req-123" # Check PII-sensitive data - if send_default_pii and include_prompts: + if send_default_pii: assert SPANDATA.MCP_TOOL_RESULT_CONTENT in span["attributes"] else: assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["attributes"] @@ -329,15 +328,15 @@ def add_numbers(a: int, b: int) -> dict: @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_fastmcp_tool_async( - sentry_init, capture_items, FastMCP, send_default_pii, include_prompts, json_rpc + sentry_init, capture_items, FastMCP, send_default_pii, json_rpc ): """Test that FastMCP async tool handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -398,7 +397,7 @@ async def multiply_numbers(x: int, y: int) -> dict: assert span["attributes"][SPANDATA.MCP_SESSION_ID] == session_id # Check PII-sensitive data - if send_default_pii and include_prompts: + if send_default_pii: assert SPANDATA.MCP_TOOL_RESULT_CONTENT in span["attributes"] else: assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in span["attributes"] @@ -464,7 +463,7 @@ async def test_fastmcp_tool_with_complex_return( ): """Test FastMCP tool with complex nested return value""" sentry_init( - integrations=[MCPIntegration(include_prompts=True)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=True, ) @@ -499,7 +498,6 @@ def get_user_data(user_id: int) -> dict: assert len(tool_spans) == 1 assert tool_spans[0]["attributes"]["sentry.op"] == OP.MCP_SERVER assert tool_spans[0]["attributes"][SPANDATA.MCP_TOOL_NAME] == "get_user_data" - # Complex return value should be captured since include_prompts=True and send_default_pii=True assert SPANDATA.MCP_TOOL_RESULT_CONTENT in tool_spans[0]["attributes"] @@ -511,20 +509,19 @@ def get_user_data(user_id: int) -> dict: @pytest.mark.asyncio @pytest.mark.parametrize("FastMCP", fastmcp_implementations, ids=fastmcp_ids) @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_fastmcp_prompt_sync( sentry_init, capture_items, FastMCP, send_default_pii, - include_prompts, stdio, ): """Test that FastMCP synchronous prompt handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -574,7 +571,7 @@ def code_help_prompt(language: str): assert span["attributes"][SPANDATA.MCP_PROMPT_NAME] == "code_help_prompt" # Check PII-sensitive data - if send_default_pii and include_prompts: + if send_default_pii: assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT in span["attributes"] else: assert SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT not in span["attributes"] diff --git a/tests/integrations/mcp/test_mcp.py b/tests/integrations/mcp/test_mcp.py index 679fd7e4db..982d25c71f 100644 --- a/tests/integrations/mcp/test_mcp.py +++ b/tests/integrations/mcp/test_mcp.py @@ -247,19 +247,18 @@ async def test_tool(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_tool_handler_stdio( sentry_init, capture_items, send_default_pii, - include_prompts, stdio, ): """Test that synchronous tool handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -314,7 +313,7 @@ async def test_tool(ctx, params): assert data["mcp.request.argument.y"] == "5" # Check PII-sensitive data is only present when both flags are True - if send_default_pii and include_prompts: + if send_default_pii: assert data[SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps( { "result": "success", @@ -329,15 +328,15 @@ async def test_tool(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_tool_handler_streamable_http( - sentry_init, capture_items, send_default_pii, include_prompts, json_rpc + sentry_init, capture_items, send_default_pii, json_rpc ): """Test that async tool handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -401,7 +400,7 @@ async def test_tool_async(ctx, params): assert data["mcp.request.argument.data"] == "test" # Check PII-sensitive data - if send_default_pii and include_prompts: + if send_default_pii: assert data[SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps( {"status": "completed"} ) @@ -509,19 +508,18 @@ async def failing_tool(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_prompt_handler_stdio( sentry_init, capture_items, send_default_pii, - include_prompts, stdio, ): """Test that synchronous prompt handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -578,7 +576,7 @@ async def test_prompt(ctx, params): assert data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 1 # For single message prompts, role and content should be captured only with PII - if send_default_pii and include_prompts: + if send_default_pii: assert data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE] == "user" assert ( data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT] == "Tell me about Python" @@ -590,15 +588,15 @@ async def test_prompt(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (True, False), (False, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_prompt_handler_streamable_http( - sentry_init, capture_items, send_default_pii, include_prompts, json_rpc + sentry_init, capture_items, send_default_pii, json_rpc ): """Test that async prompt handlers create proper spans""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -860,19 +858,18 @@ async def failing_resource(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_tool_result_extraction_tuple( sentry_init, capture_items, send_default_pii, - include_prompts, stdio, ): """Test extraction of tool results from tuple format (UnstructuredContent, StructuredContent)""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -903,7 +900,7 @@ async def test_tool_tuple(ctx, params): assert span is not None data = span["attributes"] - if send_default_pii and include_prompts: + if send_default_pii: assert data[SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps( { "key": "value", @@ -918,19 +915,18 @@ async def test_tool_tuple(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_tool_result_extraction_unstructured( sentry_init, capture_items, send_default_pii, - include_prompts, stdio, ): """Test extraction of tool results from UnstructuredContent (list of content blocks)""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -966,7 +962,7 @@ async def test_tool_unstructured(ctx, params): data = span["attributes"] # Should extract and join text from content blocks only with PII - if send_default_pii and include_prompts: + if send_default_pii: assert data[SPANDATA.MCP_TOOL_RESULT_CONTENT] == "First part Second part" else: assert SPANDATA.MCP_TOOL_RESULT_CONTENT not in data @@ -1056,19 +1052,18 @@ async def prompt_handler(ctx, params): @pytest.mark.asyncio @pytest.mark.parametrize( - "send_default_pii, include_prompts", - [(True, True), (False, False)], + "send_default_pii", + [True, False], ) async def test_prompt_with_dict_result( sentry_init, capture_items, send_default_pii, - include_prompts, stdio, ): """Test prompt handler with dict result instead of GetPromptResult object""" sentry_init( - integrations=[MCPIntegration(include_prompts=include_prompts)], + integrations=[MCPIntegration()], traces_sample_rate=1.0, send_default_pii=send_default_pii, ) @@ -1107,7 +1102,7 @@ async def test_prompt_dict(ctx, params): assert data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 1 # Role and content only captured with PII - if send_default_pii and include_prompts: + if send_default_pii: assert data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE] == "user" assert data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_CONTENT] == "Hello from dict" else: @@ -1600,38 +1595,3 @@ async def test_prompt(ctx, params): # The message count carries no prompt content, so it is never gated assert data[SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT] == 1 - - -@pytest.mark.asyncio -async def test_include_prompts_ignored_when_data_collection_set( - sentry_init, - capture_items, - stdio, -): - sentry_init( - integrations=[MCPIntegration(include_prompts=False)], - traces_sample_rate=1.0, - send_default_pii=True, - data_collection={"gen_ai": {"outputs": True}}, - ) - - server = Server("test-server") - - async def test_tool(ctx, params): - return CallToolResult( - content=[TextContent(type="text", text=json.dumps({"value": 42}))], - structured_content={"value": 42}, - ) - - server.add_request_handler("tools/call", CallToolRequestParams, test_tool) - - params = {"name": "calculate", "arguments": {"x": 10}} - items = capture_items("span") - await stdio(server, method="tools/call", params=params, request_id="req-1") - sentry_sdk.flush() - span = _find_mcp_span(items, method_name="tools/call") - assert span is not None - data = span["attributes"] - - assert data[SPANDATA.MCP_TOOL_RESULT_CONTENT] == json.dumps({"value": 42}) - assert data[SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT] == 1