diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 2d7e1b72cf1..ac90302fdaa 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -461,6 +461,7 @@ class LiteLLMRoutes(enum.Enum): "/mcp/tools/call", "/mcp-rest/tools/list", "/mcp-rest/tools/call", + "/v1/mcp/tools", ] # MCP server CRUD routes — control-plane. Gated by DISABLE_ADMIN_ENDPOINTS. diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index 52ba1dbcfbd..d623149ff6a 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -403,6 +403,48 @@ def test_virtual_key_llm_api_routes_rejects_mcp_multi_segment_admin_subpaths( assert exc_info.value.status_code == 403 +@pytest.mark.parametrize( + "route, method", + [ + ("/mcp", "POST"), + ("/mcp/", "POST"), + ("/mcp/my-server", "POST"), # matches the /mcp/{subpath} pattern + ("/mcp/tools", "GET"), + ("/mcp/tools/list", "POST"), + ("/mcp/tools/call", "POST"), + ("/mcp-rest/tools/list", "GET"), + ("/mcp-rest/tools/call", "POST"), + ("/v1/mcp/tools", "GET"), + ], +) +def test_virtual_key_llm_api_routes_allows_mcp_inference_endpoints(route, method): + """Every MCP inference/discovery endpoint must be reachable by virtual keys + scoped to allowed_routes=["llm_api_routes"], the default the Create Key UI + applies. + + /v1/mcp/tools is the most recent addition: before it joined this group a key + could list tools via /mcp/tools/list and /mcp-rest/tools/list but got a 403 + on the equivalent /v1/mcp/tools. Unlike /v1/mcp/server, none of these paths + have a management write counterpart, so they live directly in + `mcp_inference_routes` rather than behind a method-aware carve-out. + """ + + assert RouteChecks.is_llm_api_route(route=route) is True + + valid_token = UserAPIKeyAuth( + user_id="test_user", + allowed_routes=["llm_api_routes"], + ) + + result = RouteChecks.is_virtual_key_allowed_to_call_route( + route=route, + valid_token=valid_token, + request=_mock_request(method), + ) + + assert result is True + + def test_spend_logs_v2_classified_as_management_not_llm_api(): """Paginated spend logs are a management/spend read route, not an LLM API."""