From df0854745081835f20cff9a32ab54e638e19ee63 Mon Sep 17 00:00:00 2001 From: Ritwij Aryan Parmar Date: Thu, 28 May 2026 01:52:39 -0400 Subject: [PATCH] fix: capture MCP tool responses in standard logging Signed-off-by: Ritwij Aryan Parmar --- litellm/litellm_core_utils/litellm_logging.py | 9 ++ .../test_litellm_logging.py | 98 +++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 97266096ef9..405641917da 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5343,9 +5343,18 @@ def _extract_response_obj_and_hidden_params( hidden_params = getattr(init_response_obj, "_hidden_params", None) elif isinstance(init_response_obj, dict): response_obj = init_response_obj + elif callable(getattr(init_response_obj, "model_dump", None)): + response_obj = init_response_obj.model_dump() + hidden_params = getattr(init_response_obj, "_hidden_params", None) + elif callable(getattr(init_response_obj, "dict", None)): + response_obj = init_response_obj.dict() + hidden_params = getattr(init_response_obj, "_hidden_params", None) else: response_obj = {} + if not isinstance(response_obj, dict): + response_obj = {} + if original_exception is not None and hidden_params is None: response_headers = _get_response_headers(original_exception) if response_headers is not None: diff --git a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py index 07ab29c5231..f62c111667b 100644 --- a/tests/test_litellm/litellm_core_utils/test_litellm_logging.py +++ b/tests/test_litellm/litellm_core_utils/test_litellm_logging.py @@ -2689,6 +2689,104 @@ def test_get_standard_logging_object_payload_includes_litellm_call_id(logging_ob assert payload["litellm_call_id"] == call_id +def test_mcp_tool_model_dump_response_is_captured_in_standard_logging_payload( + logging_obj, +): + """MCP SDK result objects should retain tool output in standard logging.""" + import datetime + + from litellm.litellm_core_utils.litellm_logging import ( + get_standard_logging_object_payload, + ) + from litellm.types.utils import CallTypes + + class FakeMCPCallToolResult: + def model_dump(self): + return { + "content": [{"type": "text", "text": "42"}], + "structuredContent": {"answer": 42}, + "isError": False, + } + + now = datetime.datetime.now() + payload = get_standard_logging_object_payload( + kwargs={ + "litellm_call_id": "mcp-response-call-id", + "model": "MCP: math/answer", + "messages": [], + "call_type": CallTypes.call_mcp_tool.value, + "mcp_tool_call_metadata": { + "name": "answer", + "arguments": {"question": "life"}, + "mcp_server_name": "math", + "namespaced_tool_name": "math/answer", + }, + }, + init_response_obj=FakeMCPCallToolResult(), + start_time=now, + end_time=now, + logging_obj=logging_obj, + status="success", + ) + + assert payload is not None + assert payload["call_type"] == CallTypes.call_mcp_tool.value + assert payload["status"] == "success" + assert payload["response"] == { + "content": [{"type": "text", "text": "42"}], + "structuredContent": {"answer": 42}, + "isError": False, + } + assert payload["metadata"]["mcp_tool_call_metadata"] is not None + assert ( + payload["metadata"]["mcp_tool_call_metadata"]["namespaced_tool_name"] + == "math/answer" + ) + + +def test_mcp_tool_dict_response_is_captured_in_standard_logging_payload(logging_obj): + import datetime + + from litellm.litellm_core_utils.litellm_logging import ( + get_standard_logging_object_payload, + ) + from litellm.types.utils import CallTypes + + now = datetime.datetime.now() + payload = get_standard_logging_object_payload( + kwargs={ + "litellm_call_id": "mcp-json-rpc-response-call-id", + "model": "MCP: search/query", + "messages": [], + "call_type": CallTypes.call_mcp_tool.value, + }, + init_response_obj={ + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [{"type": "text", "text": "search result"}], + "structuredContent": {"ids": ["doc-1"]}, + "isError": False, + }, + }, + start_time=now, + end_time=now, + logging_obj=logging_obj, + status="success", + ) + + assert payload is not None + assert payload["response"] == { + "jsonrpc": "2.0", + "id": 1, + "result": { + "content": [{"type": "text", "text": "search result"}], + "structuredContent": {"ids": ["doc-1"]}, + "isError": False, + }, + } + + def _make_dict_logging_obj(): """Build a Logging instance configured for a non-streaming dict result.""" obj = LitellmLogging(