If you want a single line of input, it doesn't need to include the number of numbers, because you'll just split the line on whitespace to get your list.
foo :: IO [Int]
foo = do
   putStr "Enter your numbers, separated by whitespace (e.g. 4 5 3 2): "
   line <- getLine
   return $ map read (words line)
words splits an input like 2 3 4 5 into the list ["2", "3", "4", "5"].
If you want to control how many numbers are entered, with a separate prompt for each number, then you need replicateM:
-- No error handling or line editing, for simplicity
foo' :: IO [Int]
foo' = do
    putStr "How many numbers?: "
    n <- readLn
    replicateM n $ do
      putStr "Enter a number: "
      readLn
readLn is equivalent to fmap read getLine; you get a line of input, calling read on the resulting string.
replicateM takes an IO action, defined by the nested do block, as its second argument. You were passing an undefined variable action. That action produces an IO Int value, which replicateM repeats n times an combines the resulting IO Int values into a single IO [Int] value. (Which is the difference between replicateM and replicate, which would have produced a list of actions, [IO Int].)
Because foo and foo' are both declared to have type IO [Int], the compiler can infer the proper specific types for the uses of read and readLn.