diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index 731493b1337..c9f175b4fdb 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -2552,6 +2552,7 @@ if MCP_AVAILABLE: standard_logging_mcp_tool_call ) litellm_logging_obj.model = f"MCP: {name}" + litellm_logging_obj.model_call_details["model"] = f"MCP: {name}" # Resolve the MCP server early so BYOK checks and credential injection # apply to ALL dispatch paths (local tool registry AND managed MCP server). if mcp_server is None: diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py index b6550fee6b9..c9009ed526d 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server.py @@ -5489,3 +5489,84 @@ async def test_create_mcp_client_sampling_enabled(): client = await manager._create_mcp_client(server=server) assert client._sampling_callback is not None + + +@pytest.mark.asyncio +async def test_execute_mcp_tool_sets_model_in_model_call_details(): + """Regression test: MCP tools/call spend logs persisted with model="". + + execute_mcp_tool set logging_obj.model only; the spend-log writer reads + model_call_details["model"], which stays None when function_setup builds + the logging object without a "model" kwarg. + """ + import uuid + from datetime import timezone + + from litellm.proxy._experimental.mcp_server import server as mcp_module + from litellm.proxy._types import LitellmUserRoles + from litellm.utils import Rules, function_setup + + user = UserAPIKeyAuth( + api_key="sk-user", + user_id="alice", + user_role=LitellmUserRoles.INTERNAL_USER.value, + ) + + fake_server = MagicMock() + fake_server.name = "openapi-petstore" + fake_server.is_byok = False + fake_server.auth_type = None + fake_server.mcp_info = None + fake_server.server_id = "srv-1" + fake_server.server_name = "openapi-petstore" + + fake_tool = MagicMock() + fake_tool.name = "list_pets" + + start_time = datetime.now(timezone.utc) + litellm_logging_obj, _ = function_setup( + original_function="call_mcp_tool", + rules_obj=Rules(), + start_time=start_time, + litellm_call_id=str(uuid.uuid4()), + name="list_pets", + arguments={"limit": 10}, + ) + assert litellm_logging_obj.model_call_details.get("model") is None + + with ( + patch.object( + mcp_module.global_mcp_server_manager, + "_get_mcp_server_from_tool_name", + return_value=fake_server, + ), + patch.object( + mcp_module.global_mcp_server_manager, + "pre_call_tool_check", + new=AsyncMock(return_value={}), + ), + patch.object( + mcp_module.global_mcp_tool_registry, + "get_tool", + return_value=fake_tool, + ), + patch( + "litellm.proxy._experimental.mcp_server.server._handle_local_mcp_tool", + new=AsyncMock(return_value=[]), + ), + patch( + "litellm.proxy._experimental.mcp_server.server.MCPRequestHandler.is_tool_allowed", + return_value=True, + ), + ): + await mcp_module.execute_mcp_tool( + name="list_pets", + arguments={"limit": 10}, + allowed_mcp_servers=[fake_server], + start_time=start_time, + user_api_key_auth=user, + litellm_logging_obj=litellm_logging_obj, + ) + + assert litellm_logging_obj.model_call_details["model"] == "MCP: list_pets" + assert litellm_logging_obj.model == "MCP: list_pets"