Skip to content

[Bug]: mcp_tool_permissions servers not included in allowed servers list #21954

@Vaftir

Description

@Vaftir

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

Summary

When a key has mcp_tool_permissions defined for a server but that server is NOT in mcp_servers, the server is excluded from the allowed list, making the tool permissions useless.

Current Behavior

If you create a key with:

object_permission = {
    "mcp_servers": [],  # Empty
    "mcp_tool_permissions": {
        "server_id_123": ["tool_a", "tool_b"]
    }
}

The server server_id_123 is NOT accessible because _get_allowed_mcp_servers_for_key() only checks mcp_servers and mcp_access_groups, ignoring mcp_tool_permissions.keys().

Expected Behavior

Servers referenced in mcp_tool_permissions should be implicitly included in the allowed servers list. If a user defines tool permissions for a server, they clearly intend to access that server.

Root Cause

Location: litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py lines 602-614

# Current code - only checks mcp_servers and mcp_access_groups
direct_mcp_servers = key_object_permission.mcp_servers or []
access_group_servers = await MCPRequestHandler._get_mcp_servers_from_access_groups(...)

# mcp_tool_permissions.keys() is NEVER considered!
all_servers = direct_mcp_servers + access_group_servers

Same fix needed in:

  • _get_allowed_mcp_servers_for_team()
  • _get_allowed_mcp_servers_for_end_user()

Steps to Reproduce

1. Setup MCP servers

MASTER_KEY="your-master-key"

# Create public server
curl -X POST "http://localhost:4000/v1/mcp/server" \
  -H "Authorization: Bearer $MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "server_name": "public_mcp_server",
    "transport": "sse",
    "url": "http://localhost:9999/sse",
    "allow_all_keys": true
  }'

# Create private server
curl -X POST "http://localhost:4000/v1/mcp/server" \
  -H "Authorization: Bearer $MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "server_name": "private_mcp_server",
    "transport": "sse",
    "url": "http://localhost:9998/sse",
    "allow_all_keys": false
  }'

# Get the server_id of private_mcp_server
curl "http://localhost:4000/v1/mcp/server" -H "Authorization: Bearer $MASTER_KEY"
# Note: server_id is a UUID like "aa1d8e71-318a-43cb-83ad-588d56470b1b"

2. Create key with mcp_tool_permissions but empty mcp_servers

SERVER_ID="aa1d8e71-318a-43cb-83ad-588d56470b1b"  # Use actual server_id from step 1

curl -X POST "http://localhost:4000/key/generate" \
  -H "Authorization: Bearer $MASTER_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"key_alias\": \"bug_test\",
    \"object_permission\": {
      \"mcp_servers\": [],
      \"mcp_tool_permissions\": {
        \"$SERVER_ID\": [\"tool_a\", \"tool_b\"]
      }
    }
  }"

3. Verify permission was saved

SELECT mcp_servers, mcp_tool_permissions 
FROM "LiteLLM_ObjectPermissionTable" 
WHERE mcp_servers = '{}' AND mcp_tool_permissions IS NOT NULL;

-- Result:
-- mcp_servers | mcp_tool_permissions
-- {}          | {"aa1d8e71-318a-43cb-83ad-588d56470b1b": ["tool_a", "tool_b"]}

4. List MCP servers with the new key

curl "http://localhost:4000/v1/mcp/server" -H "Authorization: Bearer $NEW_KEY"

5. Observe the bug

Expected: Both public_mcp_server and private_mcp_server visible
Actual: Only public_mcp_server visible


Test Scenario Executed (Confirmation)

Environment

  • LiteLLM Version: v1.81.6
  • Database: PostgreSQL 16
  • Docker: Yes

Step 1: Create MCP Servers

Request - Create public server:

curl -X POST "http://localhost:4000/v1/mcp/server" \
  -H "Authorization: Bearer 1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "server_name": "public_mcp_server",
    "transport": "sse",
    "url": "http://localhost:9999/sse",
    "allow_all_keys": true
  }'

Response:

{
    "server_id": "e9ffaf61-16f8-4b33-8a01-e914f64d5343",
    "server_name": "public_mcp_server",
    "alias": "public_mcp_server",
    "url": "http://localhost:9999/sse",
    "transport": "sse",
    "allow_all_keys": true
}

Request - Create private server:

curl -X POST "http://localhost:4000/v1/mcp/server" \
  -H "Authorization: Bearer 1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "server_name": "private_mcp_server",
    "transport": "sse",
    "url": "http://localhost:9998/sse",
    "allow_all_keys": false
  }'

Response:

{
    "server_id": "aa1d8e71-318a-43cb-83ad-588d56470b1b",
    "server_name": "private_mcp_server",
    "alias": "private_mcp_server",
    "url": "http://localhost:9998/sse",
    "transport": "sse",
    "allow_all_keys": false
}

Step 2: Create Key with mcp_tool_permissions but empty mcp_servers

Request:

curl -X POST "http://localhost:4000/key/generate" \
  -H "Authorization: Bearer 1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "key_alias": "bug2_reproduction",
    "object_permission": {
      "mcp_servers": [],
      "mcp_tool_permissions": {
        "aa1d8e71-318a-43cb-83ad-588d56470b1b": ["tool_a", "tool_b"]
      }
    }
  }'

Response:

{
    "key": "sk-eEnFa8wtbl2Ok4nbfDZ7Yw",
    "key_alias": "bug2_reproduction",
    "token": "...",
    "object_permission": null
}

Step 3: Verify in Database

Query:

SELECT mcp_servers, mcp_tool_permissions 
FROM "LiteLLM_ObjectPermissionTable" 
WHERE mcp_servers = '{}' AND mcp_tool_permissions IS NOT NULL;

Result:

 mcp_servers |                      mcp_tool_permissions                      
-------------+----------------------------------------------------------------
 {}          | {"aa1d8e71-318a-43cb-83ad-588d56470b1b": ["tool_a", "tool_b"]}

Permission was saved correctly in database - mcp_tool_permissions contains the server_id with tool restrictions.

Step 4: List MCP Servers with Bug Key

Request:

curl "http://localhost:4000/v1/mcp/server" \
  -H "Authorization: Bearer sk-eEnFa8wtbl2Ok4nbfDZ7Yw"

Response:

[
    {
        "server_id": "e9ffaf61-16f8-4b33-8a01-e914f64d5343",
        "server_name": "public_mcp_server",
        "alias": "public_mcp_server",
        "url": "http://localhost:9999/sse",
        "transport": "sse",
        "allow_all_keys": true
    }
]

Step 5: Bug Confirmation

Expected Actual
2 servers: public_mcp_server + private_mcp_server 1 server: only public_mcp_server

BUG CONFIRMED: The key has mcp_tool_permissions defined for private_mcp_server (server_id: aa1d8e71-318a-43cb-83ad-588d56470b1b), but the server is NOT visible in the list!

Comparison: Key WITH mcp_servers populated

For comparison, when creating a key with BOTH mcp_servers and mcp_tool_permissions:

Request:

curl -X POST "http://localhost:4000/key/generate" \
  -H "Authorization: Bearer 1234567890" \
  -H "Content-Type: application/json" \
  -d '{
    "key_alias": "working_key",
    "object_permission": {
      "mcp_servers": ["aa1d8e71-318a-43cb-83ad-588d56470b1b"],
      "mcp_tool_permissions": {
        "aa1d8e71-318a-43cb-83ad-588d56470b1b": ["tool_a"]
      }
    }
  }'

List servers with this key:

[
    {
        "server_id": "aa1d8e71-318a-43cb-83ad-588d56470b1b",
        "server_name": "private_mcp_server",
        "allow_all_keys": false
    },
    {
        "server_id": "e9ffaf61-16f8-4b33-8a01-e914f64d5343",
        "server_name": "public_mcp_server",
        "allow_all_keys": true
    }
]

Works correctly when mcp_servers is populated.


Also Affects UI Dashboard

This bug also manifests in the LiteLLM UI Dashboard when configuring MCP permissions for keys/teams.

When using the UI to set mcp_tool_permissions for a server without explicitly adding that server to mcp_servers, the configuration appears to save correctly but the server remains inaccessible.

User expectation: Selecting specific tools for a server in the UI should automatically grant access to that server.

Actual behavior: The server is not accessible unless manually added to mcp_servers list as well, which is not intuitive from the UI.


Relevant log output

Tested on LiteLLM v1.81.6

Database shows permission saved correctly:
  mcp_servers: {}
  mcp_tool_permissions: {"aa1d8e71-318a-43cb-83ad-588d56470b1b": ["tool_a", "tool_b"]}

But server is not accessible via API.

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

v1.81.6

Twitter / LinkedIn details

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingproxy

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions