fix(jwt-mcp): Add mcp_routes to default team_allowed_routes

This fixes the issue where JWT auth was not selecting teams for MCP routes
because MCP routes were not in the default team_allowed_routes.

The flow was:
1. JWT auth calls find_team_with_model_access()
2. find_team_with_model_access checks allowed_routes_check() for the route
3. For MCP routes like /mcp/tools/list, this check FAILED because
   team_allowed_routes only included 'openai_routes' and 'info_routes'
4. Team was NOT selected, team_id was None
5. MCP permission lookup returned empty because team_id was None

The fix adds 'mcp_routes' to the default team_allowed_routes so that:
- Teams can be selected when accessing MCP endpoints
- Team MCP permissions (from object_permission.mcp_servers) are enforced

This is the second part of the fix for JWT + MCP permission enforcement.
The first part (allowing teams with models=None) was in the previous commit.

Changes:
- LiteLLM_JWTAuth.team_allowed_routes: Added 'mcp_routes' to default list
- allowed_routes_check: Updated fallback to also include 'mcp_routes'
- Added test_jwt_auth_allows_mcp_routes_for_teams to verify the fix

Co-authored-by: ishaan <ishaan@berri.ai>
This commit is contained in:
Cursor Agent 2026-01-30 20:17:33 +00:00
parent adbe869385
commit e3e037c927
3 changed files with 61 additions and 3 deletions

View file

@ -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",

View file

@ -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:

View file

@ -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():
"""