ConduitMcp.Plugs.MessageRateLimit (ConduitMCP v0.9.0)

Copy Markdown View Source

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 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
  • 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 IP

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()
  • :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 already supports per-user rate limiting when the ConduitMcp.Plugs.Auth plug is in the pipeline. If conn.assigns[:current_user] is set, it will be used as the key. Otherwise, the client IP is used.

You can also provide a custom key function:

message_rate_limit: [
  backend: MyApp.RateLimiter,
  limit: 50,
  key_func: fn conn ->
    "msg:custom:" <> get_custom_key(conn)
  end
]

Without message rate limiting

Simply omit the :message_rate_limit option from your transport config.