Rate limiting plug for MCP servers.
This plug is completely optional. If you don't need rate limiting, simply
omit the :rate_limit option from your transport config — no additional
dependencies are required.
Dependencies
Rate limiting requires the hammer package.
Add it to your mix.exs only if you intend to use this plug:
{:hammer, "~> 7.2"}How it works
You define your own Hammer module, supervise it in your application, and pass
it as the :backend option. This gives you full control over the backend
(:ets, :atomic), algorithm (:fix_window, :leaky_bucket, etc.), and
supervision strategy.
Options
:backend- Required when enabled. A module that implementshit/3(e.g., a module defined withuse Hammer, backend: :ets). You must supervise this module in your own application supervision tree.:enabled- Enable/disable rate limiting (default:true):scale- Time window in milliseconds (default:60_000):limit- Maximum requests per window (default:60):key_func- Function to derive the rate limit key from the connection (default: IP-based). Signature:(Plug.Conn.t()) -> String.t()
Setup
1. Define your Hammer module
defmodule MyApp.RateLimiter do
use Hammer, backend: :ets
end2. Add to your supervision tree
children = [
{MyApp.RateLimiter, [clean_period: :timer.minutes(1)]}
]3. Pass as :backend in transport config
{Bandit,
plug: {ConduitMcp.Transport.StreamableHTTP,
server_module: MyApp.MCPServer,
rate_limit: [
backend: MyApp.RateLimiter,
scale: :timer.seconds(60),
limit: 100
]},
port: 4001}Per-user rate limiting
rate_limit: [
backend: MyApp.RateLimiter,
limit: 100,
key_func: fn conn ->
case conn.assigns[:current_user] do
%{id: id} -> "user:#{id}"
_ -> conn.remote_ip |> :inet.ntoa() |> to_string()
end
end
]Without rate limiting
Simply omit the :rate_limit option from your transport config. The hammer
dependency is not required and won't be compiled.