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

Message-level rate limiting plug for MCP servers.

While the HTTP-level rate limit (`ConduitMcp.Plugs.RateLimit`) limits raw HTTP
connections, this plug limits the number of MCP **message interactions** (tool
calls, resource reads, prompt gets) a client can make per time window.

Think of it as: HTTP rate limit = "how fast can you knock on the door",
message rate limit = "how many questions can you ask once inside."

This plug is **completely optional**. If you don't need message-level rate
limiting, simply omit the `:message_rate_limit` option from your transport
config — no additional dependencies are required.

## Dependencies

Message 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

- Only POST requests are counted (GET/OPTIONS pass through)
- JSON-RPC notifications (requests without an `id` field) are not counted,
  except `notifications/cancelled`, which mutates server state
- Specific methods can be excluded (e.g., `"initialize"`, `"ping"`)
- Keys are prefixed with `"msg:"` to prevent Hammer counter collision when
  both HTTP and message rate limiters share the same backend
- Authenticated users are tracked by user ID; anonymous users by client
  bucket (`ConduitMcp.Principal.client_bucket/1`: the IPv4 address, or the
  IPv6 /64)

## 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 message rate limiting (default: `true`)
- `:scale` - Time window in milliseconds (default: `300_000` / 5 minutes)
- `:limit` - Maximum messages per window (default: `50`)
- `:key_func` - Function to derive the rate limit key from the connection
  (default: user-aware with `"msg:"` prefix). Signature: `(Plug.Conn.t()) -> String.t()`.
  Pass a remote capture (`&MyApp.RateKeys.msg/1`), not an anonymous `fn`:
  `Plug.Router.forward/2` escapes transport init opts at compile time, and
  anonymous functions cannot be escaped.
- `:excluded_methods` - List of MCP method names to skip rate limiting for
  (default: `[]`). Example: `["initialize", "ping"]`

## 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 `:message_rate_limit` in transport config

    {Bandit,
     plug: {ConduitMcp.Transport.StreamableHTTP,
            server_module: MyApp.MCPServer,
            rate_limit: [
              backend: MyApp.RateLimiter,
              scale: :timer.seconds(60),
              limit: 100
            ],
            message_rate_limit: [
              backend: MyApp.RateLimiter,
              scale: :timer.minutes(5),
              limit: 50
            ]},
     port: 4001}

## Per-user rate limiting

The default key function gives each authenticated caller their own bucket:
it keys on `ConduitMcp.Principal.id/1` — the stable scalar identity written
by `ConduitMcp.Plugs.Auth` and `ConduitMcp.Plugs.OAuth` — and falls back to
the client bucket (`ConduitMcp.Principal.client_bucket/1`: the IPv4 address,
or the IPv6 /64) when the request is unauthenticated. Two OAuth subjects
behind the same proxy therefore get distinct buckets.

To key anonymous callers on the exact address instead, or on anything else,
provide a custom key function. It must be a remote capture of a public
function:

    defmodule MyApp.RateKeys do
      def msg(conn), do: "msg:custom:" <> MyApp.custom_key(conn)
    end

    message_rate_limit: [
      backend: MyApp.RateLimiter,
      limit: 50,
      key_func: &MyApp.RateKeys.msg/1
    ]

## Without message rate limiting

Simply omit the `:message_rate_limit` option from your transport config.

---

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