From ae4d92ad509ebadbb3e72dc790f6621ca2e13de3 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Wed, 21 Jan 2026 13:54:24 +0900 Subject: [PATCH] feat: save mcp call log via responses --- litellm/responses/main.py | 3 + .../responses/mcp/chat_completions_handler.py | 2 + .../mcp/litellm_proxy_mcp_handler.py | 190 +++++++++++++++++- .../responses/mcp/mcp_streaming_iterator.py | 4 + 4 files changed, 196 insertions(+), 3 deletions(-) diff --git a/litellm/responses/main.py b/litellm/responses/main.py index 71d94287e82..78d358a1e39 100644 --- a/litellm/responses/main.py +++ b/litellm/responses/main.py @@ -301,6 +301,8 @@ async def aresponses_api_with_mcp( mcp_server_auth_headers=mcp_server_auth_headers, oauth2_headers=oauth2_headers, raw_headers=raw_headers_from_request, + litellm_call_id=kwargs.get("litellm_call_id"), + litellm_trace_id=kwargs.get("litellm_trace_id"), ) if tool_results: @@ -349,6 +351,7 @@ async def aresponses_api_with_mcp( tool_server_map=tool_server_map, base_iterator=final_response, mcp_events=tool_execution_events, + user_api_key_auth=user_api_key_auth, ) # Add custom output elements to the final response (for non-streaming) diff --git a/litellm/responses/mcp/chat_completions_handler.py b/litellm/responses/mcp/chat_completions_handler.py index 6ce59e3e67f..26853b30596 100644 --- a/litellm/responses/mcp/chat_completions_handler.py +++ b/litellm/responses/mcp/chat_completions_handler.py @@ -142,6 +142,8 @@ async def acompletion_with_mcp( mcp_server_auth_headers=mcp_server_auth_headers, oauth2_headers=oauth2_headers, raw_headers=raw_headers, + litellm_call_id=kwargs.get("litellm_call_id"), + litellm_trace_id=kwargs.get("litellm_trace_id"), ) if not tool_results: diff --git a/litellm/responses/mcp/litellm_proxy_mcp_handler.py b/litellm/responses/mcp/litellm_proxy_mcp_handler.py index 9cdcd3894e0..f5757e4d52e 100644 --- a/litellm/responses/mcp/litellm_proxy_mcp_handler.py +++ b/litellm/responses/mcp/litellm_proxy_mcp_handler.py @@ -1,3 +1,5 @@ +import traceback +from datetime import datetime from typing import ( TYPE_CHECKING, Any, @@ -11,14 +13,18 @@ from typing import ( ) from litellm._logging import verbose_logger +from litellm.constants import MAXIMUM_TRACEBACK_LINES_TO_LOG +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.proxy._experimental.mcp_server.utils import split_server_prefix_from_name from litellm.responses.main import aresponses from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator from litellm.types.llms.openai import ResponsesAPIResponse, ToolParam -from litellm.types.utils import Choices, ModelResponse +from litellm.types.utils import CallTypes, Choices, ModelResponse, StandardLoggingMCPToolCall +from litellm.utils import Rules, function_setup if TYPE_CHECKING: from mcp.types import Tool as MCPTool + from litellm.proxy.utils import ProxyLogging else: MCPTool = Any @@ -470,6 +476,8 @@ class LiteLLM_Proxy_MCP_Handler: mcp_server_auth_headers: Optional[Dict[str, Dict[str, str]]] = None, oauth2_headers: Optional[Dict[str, str]] = None, raw_headers: Optional[Dict[str, str]] = None, + litellm_call_id: Optional[str] = None, + litellm_trace_id: Optional[str] = None, ) -> List[Dict[str, Any]]: """Execute tool calls and return results.""" from fastapi import HTTPException @@ -478,10 +486,16 @@ class LiteLLM_Proxy_MCP_Handler: from litellm.proxy._experimental.mcp_server.mcp_server_manager import ( global_mcp_server_manager, ) + from litellm.proxy.proxy_server import proxy_logging_obj + + from litellm._uuid import uuid tool_results = [] tool_call_id: Optional[str] = None + rules_obj = Rules() for tool_call in tool_calls: + logging_request_data: Dict[str, Any] = {} + tool_name: str = "" try: ( tool_name, @@ -514,6 +528,101 @@ class LiteLLM_Proxy_MCP_Handler: ): sanitized_tool_name = unprefixed_name + start_time = datetime.now() + logging_input = [ + { + "role": "tool", + "content": { + "tool_name": sanitized_tool_name, + "arguments": parsed_arguments, + }, + } + ] + tool_logging_call_id = litellm_call_id or str(uuid.uuid4()) + logging_request_data: Dict[str, Any] = { + "model": f"MCP: {tool_name}", + "metadata": { + "tool_call_id": tool_call_id, + "tool_name": sanitized_tool_name, + "server_name": server_name, + }, + "input": logging_input, + "call_type": CallTypes.call_mcp_tool.value, + "litellm_call_id": tool_logging_call_id, + } + if litellm_trace_id: + logging_request_data["litellm_trace_id"] = litellm_trace_id + user_identifier = None + if user_api_key_auth is not None: + user_api_key = getattr(user_api_key_auth, "api_key", None) + if user_api_key: + logging_request_data["metadata"]["user_api_key"] = user_api_key + + user_identifier = getattr(user_api_key_auth, "end_user_id", None) or getattr( + user_api_key_auth, "user_id", None + ) + if user_identifier: + logging_request_data["user"] = user_identifier + + litellm_logging_obj: Optional[LiteLLMLoggingObj] = None + try: + litellm_logging_obj, _ = function_setup( + original_function="call_mcp_tool", + rules_obj=rules_obj, + start_time=start_time, + **logging_request_data, + ) + except Exception as logging_error: + verbose_logger.debug( + "Failed to initialize logging for MCP tool call %s: %s", + tool_name, + logging_error, + ) + litellm_logging_obj = None + + logging_request_data["litellm_logging_obj"] = litellm_logging_obj + logging_request_data["arguments"] = parsed_arguments + + if litellm_logging_obj: + try: + litellm_logging_obj.pre_call( + input=logging_input, + api_key="", + ) + except Exception: + verbose_logger.exception( + "Failed to run pre_call for MCP tool logging" + ) + + standard_logging_mcp_tool_call: StandardLoggingMCPToolCall = { + "name": sanitized_tool_name, + "arguments": parsed_arguments, + "namespaced_tool_name": tool_name, + } + mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name( + tool_name + ) + if mcp_server: + mcp_info = mcp_server.mcp_info or {} + standard_logging_mcp_tool_call["mcp_server_name"] = ( + mcp_info.get("server_name") + or getattr(mcp_server, "server_name", None) + or server_name + ) + logo_url = mcp_info.get("logo_url") + if logo_url: + standard_logging_mcp_tool_call["mcp_server_logo_url"] = logo_url + cost_info = mcp_info.get("mcp_server_cost_info") + if cost_info: + standard_logging_mcp_tool_call["mcp_server_cost_info"] = cost_info + + if litellm_logging_obj: + litellm_logging_obj.model_call_details[ + "mcp_tool_call_metadata" + ] = standard_logging_mcp_tool_call + litellm_logging_obj.model = f"MCP: {tool_name}" + litellm_logging_obj.call_type = CallTypes.call_mcp_tool.value + result = await global_mcp_server_manager.call_tool( server_name=server_name, name=sanitized_tool_name, @@ -526,6 +635,26 @@ class LiteLLM_Proxy_MCP_Handler: proxy_logging_obj=proxy_logging_obj, ) + if litellm_logging_obj: + try: + litellm_logging_obj.post_call(original_response=result) + end_time = datetime.now() + await litellm_logging_obj.async_post_mcp_tool_call_hook( + kwargs=litellm_logging_obj.model_call_details, + response_obj=result, + start_time=start_time, + end_time=end_time, + ) + await litellm_logging_obj.async_success_handler( + result=result, + start_time=start_time, + end_time=end_time, + ) + except Exception: + verbose_logger.exception( + "Failed to log MCP tool call success for %s", tool_name + ) + # Format result for inclusion in response result_text = LiteLLM_Proxy_MCP_Handler._parse_mcp_result(result) tool_results.append( @@ -537,6 +666,12 @@ class LiteLLM_Proxy_MCP_Handler: ) except BlockedPiiEntityError as e: + await LiteLLM_Proxy_MCP_Handler._log_mcp_tool_failure( + proxy_logging_obj=proxy_logging_obj, + user_api_key_auth=user_api_key_auth, + request_data=logging_request_data, + error=e, + ) verbose_logger.error( f"BlockedPiiEntityError in MCP tool call: {str(e)}" ) @@ -549,6 +684,12 @@ class LiteLLM_Proxy_MCP_Handler: } ) except GuardrailRaisedException as e: + await LiteLLM_Proxy_MCP_Handler._log_mcp_tool_failure( + proxy_logging_obj=proxy_logging_obj, + user_api_key_auth=user_api_key_auth, + request_data=logging_request_data, + error=e, + ) verbose_logger.error( f"GuardrailRaisedException in MCP tool call: {str(e)}" ) @@ -561,12 +702,28 @@ class LiteLLM_Proxy_MCP_Handler: } ) except HTTPException as e: + await LiteLLM_Proxy_MCP_Handler._log_mcp_tool_failure( + proxy_logging_obj=proxy_logging_obj, + user_api_key_auth=user_api_key_auth, + request_data=logging_request_data, + error=e, + ) verbose_logger.error(f"HTTPException in MCP tool call: {str(e)}") error_message = f"Tool call failed: {str(e.detail) if hasattr(e, 'detail') else str(e)}" tool_results.append( - {"tool_call_id": tool_call_id, "result": error_message} + { + "tool_call_id": tool_call_id, + "result": error_message, + "name": tool_name, + } ) except Exception as e: + await LiteLLM_Proxy_MCP_Handler._log_mcp_tool_failure( + proxy_logging_obj=proxy_logging_obj, + user_api_key_auth=user_api_key_auth, + request_data=logging_request_data, + error=e, + ) verbose_logger.exception(f"Error executing MCP tool call: {e}") tool_results.append( { @@ -718,6 +875,33 @@ class LiteLLM_Proxy_MCP_Handler: **call_params, ) + @staticmethod + async def _log_mcp_tool_failure( + *, + proxy_logging_obj: Optional["ProxyLogging"], + user_api_key_auth: Any, + request_data: Dict[str, Any], + error: Exception, + ) -> None: + """Log MCP tool failures via proxy logging hooks.""" + + if proxy_logging_obj is None or user_api_key_auth is None: + return + + try: + traceback_str = traceback.format_exc( + limit=MAXIMUM_TRACEBACK_LINES_TO_LOG + ) + await proxy_logging_obj.post_call_failure_hook( + request_data=request_data, + original_exception=error, + user_api_key_dict=user_api_key_auth, + route="/responses/mcp/call_tool", + traceback_str=traceback_str, + ) + except Exception: + verbose_logger.exception("Failed to log MCP tool call failure") + @staticmethod def _create_mcp_streaming_response( input: Union[str, Any], @@ -758,7 +942,7 @@ class LiteLLM_Proxy_MCP_Handler: mcp_events=mcp_discovery_events, # Pre-generated MCP discovery events tool_server_map=tool_server_map, mcp_tools_with_litellm_proxy=mcp_tools_with_litellm_proxy, - user_api_key_auth=kwargs.get("user_api_key_auth"), + user_api_key_auth=kwargs.get("user_api_key_auth") or kwargs.get("litellm_metadata", {}).get("user_api_key_auth"), original_request_params=request_params, ) diff --git a/litellm/responses/mcp/mcp_streaming_iterator.py b/litellm/responses/mcp/mcp_streaming_iterator.py index ac040d3d6ec..53f39164e91 100644 --- a/litellm/responses/mcp/mcp_streaming_iterator.py +++ b/litellm/responses/mcp/mcp_streaming_iterator.py @@ -298,6 +298,8 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): self.custom_llm_provider = self.original_request_params.get( "custom_llm_provider", None ) + self.litellm_call_id = self.original_request_params.get("litellm_call_id") + self.litellm_trace_id = self.original_request_params.get("litellm_trace_id") self._extract_mcp_headers_from_params() @@ -568,6 +570,8 @@ class MCPEnhancedStreamingIterator(BaseResponsesAPIStreamingIterator): mcp_server_auth_headers=self.mcp_server_auth_headers, oauth2_headers=self.oauth2_headers, raw_headers=self.raw_headers, + litellm_call_id=self.litellm_call_id, + litellm_trace_id=self.litellm_trace_id, ) # Create completion events and output_item.done events for tool execution