Three layers of authorization
The gem gives you three independent controls over what each user sees:
The gem gives you three independent controls over what each user sees:
| Layer | Mechanism | Effect |
|---|---|---|
| Tool visibility | authorization :manage_workflows (RBAC) or gate :feature, :sms (any predicate) on the tool class | Tool hidden entirely from users who fail any check |
| Input fields | @requires(:backward_routing) or @feature(:sms) on a param in #: annotation | Field excluded from the input schema and stripped from inbound params at call time |
| Output variants | @requires(:backward_routing) or @feature(:sms) on a variant in @rbs type output | Variant excluded from the oneOf and fields projected out of the handler's return value before it crosses the wire |
Tool-level authorization :perm is RBAC (calls current_user.can?). Tool-level gate :predicate, :value and the field-level annotations are generic โ any predicate name works, as long as the server context implements {predicate}?(value). See Generic predicate tags below.
Enforcement, not just shaping๐
@requires is a security boundary, not a hint. At tool-call time the gem:
- Filters inbound params against the user's compiled input schema. Gated fields, and any keys not declared in the schema at all, are dropped before the handler's
#callis invoked. A handler that takesforce:gated behind@requires(:admin)will seeforce: false(its default) for non-admins even if the MCP client sendsforce: truein the raw JSON-RPC payload. - Projects the handler's return value onto the user's compiled output schema. A variant hidden by
@requireshas its shape unavailable, so if the handler erroneously emits that variant's extra fields, they are stripped before serialization. A handler bug or refactor accident cannot leak admin-only fields to a non-admin.
This means handler authors don't have to remember to re-check can? at every branch -- the schema is the boundary. can? inside #call is still useful for logic that changes behavior (not just field visibility), but it is no longer load-bearing for security.
Collected from README.md in the repository. Edit it there, not here.