Skip to content

fix(mcp): stop 'Team doesn't exist' warnings for UI dashboard sessions - #32348

Merged
tin-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4227_reserved_ui_team_id
Jul 7, 2026
Merged

fix(mcp): stop 'Team doesn't exist' warnings for UI dashboard sessions#32348
tin-berri merged 1 commit into
litellm_internal_stagingfrom
litellm_lit4227_reserved_ui_team_id

Conversation

@tin-berri

@tin-berri tin-berri commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Linear ticket

Resolves LIT-4227

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Screenshots / Proof of Fix

A customer reported their proxy logs flooding with Failed to get allowed tools for server: 404: {'error': "Team doesn't exist in db. Team=litellm-dashboard. ..."}, one line per configured MCP server on every MCP tools listing from the dashboard, and was confused because they never created a team with that id. They are right that it does not exist; litellm-dashboard is the virtual team id LiteLLM stamps on every UI session token (UI_TEAM_ID), which is never persisted. The MCP team-permission helpers passed it to get_team_object anyway

Setup for both runs: fresh Postgres, 3 MCP servers pointing at the public DeepWiki MCP server, EXPERIMENTAL_UI_LOGIN=true (with plain sk- session keys the main auth flow happens to mask the 404 by caching a fabricated team object at user_api_key_auth.py:1916; the experimental UI JWT branch returns early at user_api_key_auth.py:1541 before that fallback runs, so it reproduces the exact flow the customer hit)

# lit4227_repro_config.yaml
model_list:
  - model_name: gpt-4o-mini
    litellm_params:
      model: openai/gpt-4o-mini
      api_key: os.environ/OPENAI_API_KEY
mcp_servers:
  deepwiki_1:
    url: https://mcp.deepwiki.com/mcp
    transport: http
  deepwiki_2:
    url: https://mcp.deepwiki.com/mcp
    transport: http
  deepwiki_3:
    url: https://mcp.deepwiki.com/mcp
    transport: http
general_settings:
  master_key: sk-1234
docker run -d --name lit4227-pg -e POSTGRES_PASSWORD=postgres -p 5434:5432 postgres:16
DATABASE_URL="postgresql://postgres:postgres@localhost:5434/postgres" EXPERIMENTAL_UI_LOGIN=true \
  python litellm/proxy/proxy_cli.py --config litellm/proxy/lit4227_repro_config.yaml \
  --detailed_debug --use_v2_migration_resolver 2>&1 | tee litellm.log

# mint a UI dashboard session exactly like the browser login does, pull the session token out of the cookie JWT
TOKEN=$(curl -s -i -X POST http://localhost:4000/login -d 'username=admin&password=sk-1234' \
  | grep -i '^set-cookie: token=' | sed 's/^[Ss]et-[Cc]ookie: token=//; s/;.*//')
KEY=$(python -c "import base64,json; p='$TOKEN'.split('.')[1]; p+='='*(-len(p)%4); print(json.loads(base64.urlsafe_b64decode(p))['key'])")

# MCP protocol tools/list with the UI session, same as the dashboard tool panel / any MCP client
SESSION=$(curl -s -D - -o /dev/null -X POST http://localhost:4000/mcp/ -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' \
  | grep -i '^mcp-session-id:' | tr -d '\r' | cut -d' ' -f2)
curl -s -X POST http://localhost:4000/mcp/ -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "Mcp-Session-Id: $SESSION" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | grep -o '"name":"[^"]*"' | wc -l

Before (parent commit 5e73994): tools/list returns 9 tools and the log gets one warning per configured server, the customer's exact flood

before: MCP tools/list floods the log with one 'Team doesn't exist' warning per server

       9
$ grep "Failed to get allowed tools for server" litellm.log
09:36:22 - LiteLLM:WARNING: user_api_key_auth_mcp.py:832 - Failed to get allowed tools for server: 404: {'error': "Team doesn't exist in db. Team=litellm-dashboard. Create team via `/team/new` call."}
09:36:22 - LiteLLM:WARNING: user_api_key_auth_mcp.py:832 - Failed to get allowed tools for server: 404: {'error': "Team doesn't exist in db. Team=litellm-dashboard. Create team via `/team/new` call."}
09:36:22 - LiteLLM:WARNING: user_api_key_auth_mcp.py:832 - Failed to get allowed tools for server: 404: {'error': "Team doesn't exist in db. Team=litellm-dashboard. Create team via `/team/new` call."}

Before, second defect: /team/new happily creates a real team with the reserved id, whose budget and permissions would then bind to every UI session because the auth flow resolves the session's team against the DB row

before: /team/new accepts the reserved id and the row persists in the DB

$ curl -s -X POST http://localhost:4000/team/new -H 'Authorization: Bearer sk-1234' -H 'Content-Type: application/json' \
    -d '{"team_id": "litellm-dashboard", "team_alias": "innocent-looking-team", "max_budget": 0.01}'
{"team_alias": "innocent-looking-team", "team_id": "litellm-dashboard", ...}   # 200

$ docker exec lit4227-pg psql -U postgres -t -c 'SELECT team_id, team_alias, max_budget FROM "LiteLLM_TeamTable";'
 litellm-dashboard | innocent-looking-team |       0.01

After (this PR, e84cefa), same commands: tools/list still returns the same 9 tools and the log stays clean

after: MCP tools/list still returns 9 tools and the warning count is 0

       9
$ grep -c "Failed to get allowed tools for server" litellm.log
0

After, /team/new rejects the reserved id and regular team creation is untouched

after: /team/new returns HTTP 400 for the reserved id, only the regular team persists

$ curl -s -w "\nHTTP status: %{http_code}\n" -X POST http://localhost:4000/team/new -H 'Authorization: Bearer sk-1234' \
    -H 'Content-Type: application/json' -d '{"team_id": "litellm-dashboard", "team_alias": "innocent-looking-team", "max_budget": 0.01}'
{"error":{"message":"{'error': \"team_id 'litellm-dashboard' is reserved for LiteLLM UI dashboard sessions and cannot be used for a real team. Please use a different team id.\"}","type":"internal_server_error","param":"None","code":"400"}}
HTTP status: 400

$ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:4000/team/new -H 'Authorization: Bearer sk-1234' \
    -H 'Content-Type: application/json' -d '{"team_id": "regular-team-1", "team_alias": "regular team"}'
200

Type

🐛 Bug Fix

Changes

MCPRequestHandler._get_team_object_permission, _get_allowed_mcp_servers_for_team and _get_mcp_access_groups_for_team now short-circuit to their empty result when user_api_key_auth.team_id == UI_TEAM_ID, before hitting get_team_object. The virtual UI team can never have a DB row, so the lookup always raised HTTPException 404, which each helper swallowed into its own WARNING (the tools one fires once per server per tools/list, producing the flood) plus a wasted DB query. The 404 also aborted get_allowed_tools_for_server before the key-level tool permission handling ran, silently dropping key tool restrictions for such sessions; with the short-circuit the key/agent/org intersections apply normally. This mirrors the existing UI_TEAM_ID handling in agent_permission_handler.py

/team/new now rejects team_id == UI_TEAM_ID with a 400 before the duplicate check, so nobody can create a real team under the reserved id and silently attach its budget and permissions to every UI dashboard session

Regression tests: the three MCP helpers return their empty result for the UI team without calling get_team_object (asserted via assert_not_called), get_allowed_tools_for_server keeps key-level tool restrictions when the team lookup would 404, and /team/new returns 400 for the reserved id without reaching the DB duplicate check. All of them fail on the parent commit and pass with this change

Link to Devin session: https://app.devin.ai/sessions/f03da2725ec94d28b3facf766871b102


Note

Low Risk
Targeted auth guards on a known virtual team id plus team-creation validation; behavior for real teams is unchanged and tests cover the main paths.

Overview
Stops MCP team-permission code from calling get_team_object when the session carries the virtual dashboard team id UI_TEAM_ID (litellm-dashboard). _get_team_object_permission, _get_allowed_mcp_servers_for_team, and _get_mcp_access_groups_for_team now return empty/None immediately, which removes per-server "Team doesn't exist" log noise on dashboard MCP tools/list and avoids a regression where a 404 from team lookup could skip key-level MCP tool restrictions.

/team/new now returns 400 if team_id is UI_TEAM_ID, so operators cannot create a real team row that would bind budget and permissions to every UI session.

Regression tests cover the MCP short-circuits, preserved key tool permissions for UI sessions, and rejection of the reserved id on team creation.

Reviewed by Cursor Bugbot for commit e84cefa. Bugbot is set up for automated code reviews on this repo. Configure here.

UI session tokens carry the virtual team_id litellm-dashboard (UI_TEAM_ID),
which is never persisted. The MCP team-permission helpers passed it to
get_team_object anyway, so every dashboard MCP listing raised a 404 per
lookup that was swallowed into per-server 'Failed to get allowed tools for
server' warnings (plus the sibling 'allowed MCP servers for team' and 'MCP
access groups for team' warnings) and wasted DB queries. The 404 also
escaped past the key-level permission handling in
get_allowed_tools_for_server, dropping key tool restrictions for such
sessions.

Short-circuit the virtual team before the DB lookup in the three helpers,
mirroring the existing UI_TEAM_ID handling in agent_permission_handler.
Also reject /team/new with the reserved team_id, since a real row would
bind its budget and permissions to every UI session
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes spurious "Team doesn't exist in db. Team=litellm-dashboard" warnings flooding proxy logs whenever the UI dashboard session listed MCP tools, and closes a privilege-escalation path where POST /team/new could create a real DB row under the reserved litellm-dashboard ID.

  • Three MCP permission helpers (_get_team_object_permission, _get_allowed_mcp_servers_for_team, _get_mcp_access_groups_for_team) now short-circuit to their zero-value when team_id == UI_TEAM_ID, eliminating the O(n-servers) 404 flood and a silent fail-open for key-level tool restrictions.
  • POST /team/new rejects the reserved ID with HTTP 400 before touching the DB, preventing a rogue team row from binding budget/permission limits to every UI session.
  • Four focused unit tests validate all the new guards (no DB call made) and the key-restriction regression case.

Confidence Score: 5/5

Safe to merge — the changes are narrow, additive guards on a well-understood constant, and the existing auth critical path is unaffected.

All three MCP helpers and the team creation endpoint receive minimal, targeted guards with no changes to surrounding logic. The pattern mirrors the already-merged handling in agent_permission_handler.py. The new tests fail on the parent commit and pass here, the get_team_object mock correctly captures the source-module attribute, and ProxyException.code is stringified so the '400' assertion is valid.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py Adds UI_TEAM_ID short-circuit guards in _get_team_object_permission, _get_allowed_mcp_servers_for_team, and _get_mcp_access_groups_for_team to prevent spurious DB lookups and log floods for virtual UI dashboard sessions.
litellm/proxy/management_endpoints/team_endpoints.py Adds a reserved-ID guard in new_team that rejects UI_TEAM_ID before the duplicate-check DB query, preventing a real team row from shadowing every UI session's budget and permissions.
tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py Adds four new async tests covering the UI_TEAM_ID short-circuits: the three helpers return their zero-value without calling get_team_object, and get_allowed_tools_for_server still applies key-level tool restrictions for UI sessions.
tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py Adds a test asserting that /team/new raises a 400 ProxyException for the reserved UI_TEAM_ID and that the DB duplicate-check query is never reached.

Reviews (1): Last reviewed commit: "fix(mcp): stop 'Team doesn't exist' warn..." | Re-trigger Greptile

@tin-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e84cefa. Configure here.

@tin-berri
tin-berri merged commit 7ce573e into litellm_internal_staging Jul 7, 2026
131 checks passed
@tin-berri
tin-berri deleted the litellm_lit4227_reserved_ui_team_id branch July 7, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants