# `ConduitMcp.Plugs.RateLimit`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/plugs/rate_limit.ex#L1)

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`](https://hex.pm/packages/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 implements `hit/3`
  (e.g., a module defined with `use 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: the client's IPv4 address or IPv6 `/64`, see below).
  Signature: `(Plug.Conn.t()) -> String.t()`.
  Pass a remote capture (`&MyApp.RateKeys.http/1`), not an anonymous `fn`:
  `Plug.Router.forward/2` escapes transport init opts at compile time, and
  anonymous functions cannot be escaped.

## Setup

### 1. Define your Hammer module

    defmodule MyApp.RateLimiter do
      use Hammer, backend: :ets
    end

### 2. 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}

## Default key

The default key is `ConduitMcp.Principal.client_bucket/1`, because this plug
bounds raw connections including unauthenticated ones: the address for an
IPv4 client, and the `/64` prefix (`"2001:db8:1:2::/64"`) for an IPv6 one.
One host is typically allocated a whole `/64` and picks the low 64 bits
itself, so a per-address key would let it rotate addresses for a fresh
bucket on every request. IPv4-mapped IPv6 addresses (`::ffff:192.0.2.1`)
count as the embedded IPv4 address.

To key on the precise address instead — for example when every client
behind one `/64` is a distinct tenant you trust — pass
`ConduitMcp.Principal.client_ip/1`:

    rate_limit: [
      backend: MyApp.RateLimiter,
      key_func: &ConduitMcp.Principal.client_ip/1
    ]

## Per-user rate limiting

To bucket by authenticated caller instead, use the canonical principal
(anonymous callers still fall back to `ConduitMcp.Principal.client_bucket/1`):

    rate_limit: [
      backend: MyApp.RateLimiter,
      limit: 100,
      key_func: &ConduitMcp.Principal.rate_limit_key/1
    ]

Do not hand-roll `conn.remote_ip |> :inet.ntoa() |> to_string()` in a custom
key function: `:inet.ntoa/1` returns `{:error, :einval}` for a malformed
address and `to_string/1` on that raises, killing the request process
instead of returning 429. `ConduitMcp.Principal.client_ip/1` and
`ConduitMcp.Principal.client_bucket/1` return `"unknown"` instead.

## Without rate limiting

Simply omit the `:rate_limit` option from your transport config. The `hammer`
dependency is not required and won't be compiled.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
