diff --git a/.gitignore b/.gitignore index c2ac5137cb..e1045032d4 100644 --- a/.gitignore +++ b/.gitignore @@ -97,3 +97,5 @@ litellm_config.yaml .vscode/launch.json litellm/proxy/to_delete_loadtest_work/* update_model_cost_map.py +tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +litellm/proxy/_experimental/out/guardrails/index.html diff --git a/litellm/proxy/_experimental/out/api-reference.html b/litellm/proxy/_experimental/out/api-reference/index.html similarity index 100% rename from litellm/proxy/_experimental/out/api-reference.html rename to litellm/proxy/_experimental/out/api-reference/index.html diff --git a/litellm/proxy/_experimental/out/guardrails.html b/litellm/proxy/_experimental/out/guardrails.html deleted file mode 100644 index fb918264d2..0000000000 --- a/litellm/proxy/_experimental/out/guardrails.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/logs.html b/litellm/proxy/_experimental/out/logs/index.html similarity index 100% rename from litellm/proxy/_experimental/out/logs.html rename to litellm/proxy/_experimental/out/logs/index.html diff --git a/litellm/proxy/_experimental/out/model-hub.html b/litellm/proxy/_experimental/out/model-hub/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model-hub.html rename to litellm/proxy/_experimental/out/model-hub/index.html diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub_table.html rename to litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm/proxy/_experimental/out/models-and-endpoints.html b/litellm/proxy/_experimental/out/models-and-endpoints/index.html similarity index 100% rename from litellm/proxy/_experimental/out/models-and-endpoints.html rename to litellm/proxy/_experimental/out/models-and-endpoints/index.html diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding.html deleted file mode 100644 index 5df786e1f1..0000000000 --- a/litellm/proxy/_experimental/out/onboarding.html +++ /dev/null @@ -1 +0,0 @@ -LiteLLM Dashboard \ No newline at end of file diff --git a/litellm/proxy/_experimental/out/organizations.html b/litellm/proxy/_experimental/out/organizations/index.html similarity index 100% rename from litellm/proxy/_experimental/out/organizations.html rename to litellm/proxy/_experimental/out/organizations/index.html diff --git a/litellm/proxy/_experimental/out/teams.html b/litellm/proxy/_experimental/out/teams/index.html similarity index 100% rename from litellm/proxy/_experimental/out/teams.html rename to litellm/proxy/_experimental/out/teams/index.html diff --git a/litellm/proxy/_experimental/out/test-key.html b/litellm/proxy/_experimental/out/test-key/index.html similarity index 100% rename from litellm/proxy/_experimental/out/test-key.html rename to litellm/proxy/_experimental/out/test-key/index.html diff --git a/litellm/proxy/_experimental/out/usage.html b/litellm/proxy/_experimental/out/usage/index.html similarity index 100% rename from litellm/proxy/_experimental/out/usage.html rename to litellm/proxy/_experimental/out/usage/index.html diff --git a/litellm/proxy/_experimental/out/users.html b/litellm/proxy/_experimental/out/users/index.html similarity index 100% rename from litellm/proxy/_experimental/out/users.html rename to litellm/proxy/_experimental/out/users/index.html diff --git a/litellm/proxy/_experimental/out/virtual-keys.html b/litellm/proxy/_experimental/out/virtual-keys/index.html similarity index 100% rename from litellm/proxy/_experimental/out/virtual-keys.html rename to litellm/proxy/_experimental/out/virtual-keys/index.html diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py index 0476f290a0..bfb2f4bf19 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -1222,6 +1222,104 @@ class TestMCPServerManager: "Contact proxy admin to allow this tool" in exc_info.value.detail["error"] ) + @pytest.mark.asyncio + async def test_call_tool_without_broken_pipe_error(self): + """ + Test that call_tool properly uses async context manager to avoid broken pipe errors. + This test ensures that tasks are awaited INSIDE the context manager, keeping the connection alive. + """ + from unittest.mock import AsyncMock, MagicMock, patch + + from mcp.types import CallToolResult + + manager = MCPServerManager() + + # Create a test server + server = MCPServer( + server_id="test-server", + name="test-server", + transport=MCPTransport.http, + url="http://test-server.com", + ) + + # Register the server and map a tool to it + manager.registry = {"test-server": server} + manager.tool_name_to_mcp_server_name_mapping["test_tool"] = "test-server" + + # Create mock client that tracks context manager usage + mock_client = MagicMock() + context_entered = False + context_exited = False + call_tool_called_inside_context = False + + async def mock_aenter(self): + nonlocal context_entered + context_entered = True + return self + + async def mock_aexit(self, exc_type, exc_val, exc_tb): + nonlocal context_exited + context_exited = True + # Verify that call_tool was called before context exit + assert ( + call_tool_called_inside_context + ), "call_tool must be awaited inside context manager" + return False + + async def mock_call_tool(params): + nonlocal call_tool_called_inside_context + # Verify we're inside the context when this is called + assert context_entered, "call_tool called outside context manager" + assert not context_exited, "call_tool called after context exit" + call_tool_called_inside_context = True + + # Return a mock CallToolResult + result = MagicMock(spec=CallToolResult) + result.content = [{"type": "text", "text": "Tool executed successfully"}] + result.isError = False + return result + + mock_client.__aenter__ = mock_aenter + mock_client.__aexit__ = mock_aexit + mock_client.call_tool = mock_call_tool + + # Mock _create_mcp_client to return our mock client + manager._create_mcp_client = MagicMock(return_value=mock_client) + + # Mock user auth with no restrictions + user_api_key_auth = MagicMock() + user_api_key_auth.object_permission = None + user_api_key_auth.object_permission_id = None + + # Mock proxy logging + proxy_logging_obj = MagicMock() + proxy_logging_obj._create_mcp_request_object_from_kwargs = MagicMock( + return_value={} + ) + proxy_logging_obj._convert_mcp_to_llm_format = MagicMock(return_value={}) + proxy_logging_obj.pre_call_hook = AsyncMock(return_value={}) + proxy_logging_obj.during_call_hook = AsyncMock(return_value=None) + + # Call the tool + result = await manager.call_tool( + name="test_tool", + arguments={"param": "value"}, + user_api_key_auth=user_api_key_auth, + proxy_logging_obj=proxy_logging_obj, + ) + + # Verify the result + assert result is not None + assert result.isError is False + assert len(result.content) > 0 + + # Verify context manager was used properly + assert context_entered, "Context manager __aenter__ was not called" + assert context_exited, "Context manager __aexit__ was not called" + assert ( + call_tool_called_inside_context + ), "call_tool was not awaited inside context" + if __name__ == "__main__": pytest.main([__file__])