fix: capture MCP tool responses in standard logging

Signed-off-by: Ritwij Aryan Parmar <ritwij.aryan.parmar@gmail.com>
This commit is contained in:
Ritwij Aryan Parmar 2026-05-28 01:52:39 -04:00
parent 5699a06413
commit df08547450
2 changed files with 107 additions and 0 deletions

View file

@ -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:

View file

@ -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(