mcp_authorization v0.7.1

Caching tools/list

tools/list must materialize a per-user schema for every tool in a domain. That cost can be cached. Default is no caching, so nothing changes unless you opt in:

On this page

tools/list must materialize a per-user schema for every tool in a domain. That cost can be cached. Default is no caching, so nothing changes unless you opt in:

McpAuthorization.configure do |c|
  c.tools_list_cache = :redis          # or :memory, or any object responding to get/set
  c.tools_list_cache_ttl = 3600        # seconds (default)
end
StoreBehavior
:memoryProcess-local, bounded LRU with per-entry TTL
:redisShared across processes, JSON values, per-entry TTL
custom objectAnything responding to get/set
(unset)NullStore โ€” no caching

The Redis connection resolves from an explicit client (tools_list_cache_redis), then tools_list_cache_redis_url, then ENV["REDIS_URL"], then a bare Redis.new โ€” i.e. it defaults to the host's Rails redis config with no extra wiring. redis is an optional dependency, required lazily only when the Redis store is used.

The key is a decision vector, not an identity๐Ÿ”—

H(domain + tool_defs_digest + vocab_fingerprint + decision_vector)

The decision vector is the result of every gating decision the domain's compilation consults โ€” @requires / @feature / @tier / custom predicates, tool-level gate and authorization, and current_user.can? / default_for. It never includes user or account identity.

Two contexts that answer all of those identically produce identical schemas by construction, so they share an entry. Flip one feature flag and the vector โ€” and the key โ€” change, so an admin in a flag-on account never receives a flag-off account's tools. The tool_defs_digest (each tool's gates plus handler source, plus facet configuration) changes on deploy, auto-invalidating stale entries; the TTL bounds out-of-band staleness, such as a permission changed directly in the database.

Two ways to supply the vector๐Ÿ”—

Automatic. On the first (cold) compile the gem wraps the context in a Cache::Recorder, learns the domain's predicate vocabulary, then replays that vocabulary against the live context on later requests.

Explicit. If the server context responds to mcp_cache_fingerprint, its return value is used verbatim as the decision component and the recorder is skipped:

class ServerContext
  def mcp_cache_fingerprint
    [current_user.role, account.enabled_features.sort, account.plan_tier]
  end
end

Explicit wins when present. Reach for it when gating depends on something the recorder cannot observe, or when you would rather own the invalidation contract than infer it.

Operational notes๐Ÿ”—

  • Cache outages fail open โ€” a get/set error is logged and behaves as a miss, never breaking tools/list.
  • Only successful listings are cached. Error and unexpected responses render but are not stored.
  • A hit still rebuilds the envelope โ€” the cached result is re-wrapped with the live JSON-RPC id.
  • Development reloads clear it (the reloader calls Cache.reset!), so an edited annotation shows up immediately.

Collected from README.md in the repository. Edit it there, not here.