diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index bf99347ef6e..8ebf940b9ca 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3672,7 +3672,7 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): team_id_upsert: bool = False team_ids_jwt_field: Optional[str] = None upsert_sso_user_to_team: bool = False - team_allowed_routes: List[str] = ["openai_routes", "info_routes"] + team_allowed_routes: List[str] = ["openai_routes", "info_routes", "mcp_routes"] team_id_default: Optional[str] = Field( default=None, description="If no team_id given, default permissions/spend-tracking to this team.s", diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index e0b056d450f..67bebe0b86e 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -459,10 +459,11 @@ def allowed_routes_check( elif user_role == LitellmUserRoles.TEAM: if litellm_proxy_roles.team_allowed_routes is None: """ - By default allow a team to call openai + info routes + By default allow a team to call openai + info + mcp routes """ is_allowed = _allowed_routes_check( - user_route=user_route, allowed_routes=["openai_routes", "info_routes"] + user_route=user_route, + allowed_routes=["openai_routes", "info_routes", "mcp_routes"], ) return is_allowed elif litellm_proxy_roles.team_allowed_routes is not None: diff --git a/tests/test_litellm/proxy/auth/test_handle_jwt.py b/tests/test_litellm/proxy/auth/test_handle_jwt.py index e05792851a1..78978e6cd90 100644 --- a/tests/test_litellm/proxy/auth/test_handle_jwt.py +++ b/tests/test_litellm/proxy/auth/test_handle_jwt.py @@ -962,6 +962,63 @@ async def test_jwt_auth_sets_team_id_for_mcp_permission_lookup(monkeypatch): "Team should have object_permission_id for MCP permissions" +@pytest.mark.asyncio +async def test_jwt_auth_allows_mcp_routes_for_teams(monkeypatch): + """ + Test that JWT auth properly selects a team when accessing MCP routes. + + This tests the fix for the issue where MCP routes were not in the default + team_allowed_routes, causing teams to not be selected for MCP requests, + which meant team MCP permissions were not enforced. + """ + from litellm.caching import DualCache + from litellm.proxy.utils import ProxyLogging + import sys + import types + + proxy_server_module = types.ModuleType("proxy_server") + proxy_server_module.llm_router = None + monkeypatch.setitem(sys.modules, "litellm.proxy.proxy_server", proxy_server_module) + + # Team with models (standard team with model access) + team = LiteLLM_TeamTable( + team_id="team-with-models", + models=["gpt-4", "claude-sonnet"], + object_permission_id="obj-perm-456", + ) + + async def mock_get_team_object(*args, **kwargs): + return team + + monkeypatch.setattr( + "litellm.proxy.auth.handle_jwt.get_team_object", mock_get_team_object + ) + + jwt_handler = JWTHandler() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth() + + user_api_key_cache = DualCache() + proxy_logging_obj = ProxyLogging(user_api_key_cache=user_api_key_cache) + + # Test with MCP route - this should now work with the fix + team_id, team_obj = await JWTAuthManager.find_team_with_model_access( + team_ids={"team-with-models"}, + requested_model=None, # MCP requests don't specify a model + route="/mcp/tools/list", # MCP route - now allowed for teams + jwt_handler=jwt_handler, + prisma_client=None, + user_api_key_cache=user_api_key_cache, + parent_otel_span=None, + proxy_logging_obj=proxy_logging_obj, + ) + + # Team should be selected for MCP routes + assert team_id == "team-with-models", \ + "Team should be selected for MCP routes (mcp_routes now in team_allowed_routes)" + assert team_obj.team_id == "team-with-models" + assert team_obj.object_permission_id == "obj-perm-456" + + @pytest.mark.asyncio async def test_auth_builder_returns_team_membership_object(): """