diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 8e83aea69a8..8dcd7326866 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -67,6 +67,7 @@ from litellm.types.utils import ( StandardCallbackDynamicParams, StandardLoggingAdditionalHeaders, StandardLoggingHiddenParams, + StandardLoggingMCPToolCall, StandardLoggingMetadata, StandardLoggingModelCostFailureDebugInformation, StandardLoggingModelInformation, @@ -3114,6 +3115,7 @@ class StandardLoggingPayloadSetup: litellm_params: Optional[dict] = None, prompt_integration: Optional[str] = None, applied_guardrails: Optional[List[str]] = None, + mcp_tool_call_metadata: Optional[StandardLoggingMCPToolCall] = None, ) -> StandardLoggingMetadata: """ Clean and filter the metadata dictionary to include only the specified keys in StandardLoggingMetadata. @@ -3160,6 +3162,7 @@ class StandardLoggingPayloadSetup: user_api_key_end_user_id=None, prompt_management_metadata=prompt_management_metadata, applied_guardrails=applied_guardrails, + mcp_tool_call_metadata=mcp_tool_call_metadata, ) if isinstance(metadata, dict): # Filter the metadata dictionary to include only the specified keys @@ -3486,6 +3489,7 @@ def get_standard_logging_object_payload( litellm_params=litellm_params, prompt_integration=kwargs.get("prompt_integration", None), applied_guardrails=kwargs.get("applied_guardrails", None), + mcp_tool_call_metadata=kwargs.get("mcp_tool_call_metadata", None), ) _request_body = proxy_server_request.get("body", {}) @@ -3626,6 +3630,7 @@ def get_standard_logging_metadata( user_api_key_end_user_id=None, prompt_management_metadata=None, applied_guardrails=None, + mcp_tool_call_metadata=None, ) if isinstance(metadata, dict): # Filter the metadata dictionary to include only the specified keys diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 32c33904a3b..0a0ec133402 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -11,11 +11,13 @@ from fastapi.responses import StreamingResponse from pydantic import ValidationError from litellm._logging import verbose_logger +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.types.mcp_server.mcp_server_manager import ( ListMCPToolsRestAPIResponseObject, ) +from litellm.types.utils import StandardLoggingMCPToolCall from litellm.utils import client # Check if MCP is available @@ -124,6 +126,20 @@ if MCP_AVAILABLE: status_code=400, detail="Request arguments are required" ) + standard_logging_mcp_tool_call: StandardLoggingMCPToolCall = ( + _get_standard_logging_mcp_tool_call( + name=name, + arguments=arguments, + ) + ) + litellm_logging_obj: Optional[LiteLLMLoggingObj] = kwargs.get( + "litellm_logging_obj", None + ) + if litellm_logging_obj: + litellm_logging_obj.model_call_details["mcp_tool_call_metadata"] = ( + standard_logging_mcp_tool_call + ) + # Try managed server tool first if name in global_mcp_server_manager.tool_name_to_mcp_server_name_mapping: return await _handle_managed_mcp_tool(name, arguments) @@ -131,6 +147,25 @@ if MCP_AVAILABLE: # Fall back to local tool registry return await _handle_local_mcp_tool(name, arguments) + def _get_standard_logging_mcp_tool_call( + name: str, + arguments: Dict[str, Any], + ) -> StandardLoggingMCPToolCall: + mcp_server = global_mcp_server_manager._get_mcp_server_from_tool_name(name) + if mcp_server: + mcp_info = mcp_server.mcp_info or {} + return StandardLoggingMCPToolCall( + name=name, + arguments=arguments, + mcp_server_name=mcp_info.get("server_name"), + mcp_server_logo_url=mcp_info.get("logo_url"), + ) + else: + return StandardLoggingMCPToolCall( + name=name, + arguments=arguments, + ) + async def _handle_managed_mcp_tool( name: str, arguments: Dict[str, Any] ) -> List[Union[MCPTextContent, MCPImageContent, MCPEmbeddedResource]]: diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index e7d6bec0044..963d6762bf4 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -27,6 +27,7 @@ from litellm.types.utils import ( ModelResponse, ProviderField, StandardCallbackDynamicParams, + StandardLoggingMCPToolCall, StandardLoggingPayloadErrorInformation, StandardLoggingPayloadStatus, StandardPassThroughResponseObject, @@ -1928,6 +1929,7 @@ class SpendLogsMetadata(TypedDict): ] # special param to log k,v pairs to spendlogs for a call requester_ip_address: Optional[str] applied_guardrails: Optional[List[str]] + mcp_tool_call_metadata: Optional[StandardLoggingMCPToolCall] status: StandardLoggingPayloadStatus proxy_server_request: Optional[str] batch_models: Optional[List[str]] diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 6e9a0880775..096c5191b18 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -13,7 +13,7 @@ from litellm._logging import verbose_proxy_logger from litellm.litellm_core_utils.core_helpers import get_litellm_metadata_from_kwargs from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload from litellm.proxy.utils import PrismaClient, hash_token -from litellm.types.utils import StandardLoggingPayload +from litellm.types.utils import StandardLoggingMCPToolCall, StandardLoggingPayload from litellm.utils import get_end_user_id_for_cost_tracking @@ -38,6 +38,7 @@ def _get_spend_logs_metadata( metadata: Optional[dict], applied_guardrails: Optional[List[str]] = None, batch_models: Optional[List[str]] = None, + mcp_tool_call_metadata: Optional[StandardLoggingMCPToolCall] = None, ) -> SpendLogsMetadata: if metadata is None: return SpendLogsMetadata( @@ -55,6 +56,7 @@ def _get_spend_logs_metadata( error_information=None, proxy_server_request=None, batch_models=None, + mcp_tool_call_metadata=None, ) verbose_proxy_logger.debug( "getting payload for SpendLogs, available keys in metadata: " @@ -71,6 +73,7 @@ def _get_spend_logs_metadata( ) clean_metadata["applied_guardrails"] = applied_guardrails clean_metadata["batch_models"] = batch_models + clean_metadata["mcp_tool_call_metadata"] = mcp_tool_call_metadata return clean_metadata @@ -200,6 +203,11 @@ def get_logging_payload( # noqa: PLR0915 if standard_logging_payload is not None else None ), + mcp_tool_call_metadata=( + standard_logging_payload["metadata"].get("mcp_tool_call_metadata", None) + if standard_logging_payload is not None + else None + ), ) special_usage_fields = ["completion_tokens", "prompt_tokens", "total_tokens"] diff --git a/litellm/types/utils.py b/litellm/types/utils.py index fe6330f8bdf..33522abe06c 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -1647,6 +1647,33 @@ class StandardLoggingUserAPIKeyMetadata(TypedDict): user_api_key_end_user_id: Optional[str] +class StandardLoggingMCPToolCall(TypedDict, total=False): + name: str + """ + Name of the tool to call + """ + arguments: dict + """ + Arguments to pass to the tool + """ + result: dict + """ + Result of the tool call + """ + + mcp_server_name: Optional[str] + """ + Name of the MCP server that the tool call was made to + """ + + mcp_server_logo_url: Optional[str] + """ + Optional logo URL of the MCP server that the tool call was made to + + (this is to render the logo on the logs page on litellm ui) + """ + + class StandardBuiltInToolsParams(TypedDict, total=False): """ Standard built-in OpenAItools parameters @@ -1677,6 +1704,7 @@ class StandardLoggingMetadata(StandardLoggingUserAPIKeyMetadata): requester_ip_address: Optional[str] requester_metadata: Optional[dict] prompt_management_metadata: Optional[StandardLoggingPromptManagementMetadata] + mcp_tool_call_metadata: Optional[StandardLoggingMCPToolCall] applied_guardrails: Optional[List[str]]