mcp_authorization 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 79a43a7ab018242f92e8f0448f73ae5acbae410ebd7397868ec52553490a15ec
4
- data.tar.gz: 2f41f4e5cfc4a923b26636551f25facf94c2c902be106ea19235a0c46b68dae6
3
+ metadata.gz: 9042f13cf2b294336b81ade0d8f97c31179a6483dde8c73c2258a7459227c7c4
4
+ data.tar.gz: 8be4296941e757f985bdab130472aa3f7f1bda837eae5d3b8d924078e9f27bb9
5
5
  SHA512:
6
- metadata.gz: 07fc7850bfa4960c00aee35f3543e907d5b1037ffe4b45a46b247e002d3d9a8271d4a7070746c031a2cd03d498a93a496bf1a74c998f17c1023fe361f416e27b
7
- data.tar.gz: f83d36ff6ff7b43fffad639f8e81790a20c450959fe1cadb5d009ff39dee41464199e9d636ad1e1048d32e053eba84b4239d915e9837fcde11a495eb48361642
6
+ metadata.gz: f167384e7a8b591bb76e05677ed8f99998bcb253144f509046cb4c46e57170ef8348350b3d926a04620011e4982d91de354e41be4c01e39f08acdcec06b893ff
7
+ data.tar.gz: c283b35338579177226136f6fb66d778e982e9748e5d946e8ec0232134149aeaa3571033133923b5caf7c497aba14d9b2798a3c5f784c2dc50a22cdcb7e907a4
data/CHANGELOG.md CHANGED
@@ -4,6 +4,30 @@ All notable changes to this gem are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
5
5
  adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [0.4.0] - 2026-05-21
8
+
9
+ ### Added
10
+ - **Tool-level generic predicate gates.** `Tool` subclasses can now declare any number of `gate :predicate_name, :value` calls in addition to `authorization :perm`. The gate is evaluated at request time by calling `server_context.{predicate_name}?(value)`; if any gate returns false, the tool is hidden from `tools/list` and rejected from `tools/call`. This is the tool-level counterpart of the field-level `@predicate(:value)` system introduced in 0.3.0 — same semantics, same fail-open + error-isolation behavior, same backward-compat fallback for `gate :requires`.
11
+
12
+ ```ruby
13
+ class BulkSendSmsTool < McpAuthorization::Tool
14
+ authorization :communications # RBAC permission (existing behavior preserved)
15
+ gate :feature, :sms # hide tool unless current_account.sms_enabled?
16
+ gate :requires, :super_user # extra check beyond authorization
17
+ end
18
+ ```
19
+
20
+ - `McpAuthorization::Diagnostics` module — shared helper for the development-mode "Did you mean?" warning previously duplicated between field-level (`@predicate`) and tool-level (`gate`) sites. Single Levenshtein implementation, single warning phrasing per call site (`gate :feture, :sms` warns "Did you mean gate :feature?", `@feture(:sms)` warns "Did you mean @feature?").
21
+
22
+ ### Changed
23
+ - **`authorization` migrated to the generic gate pipeline.** `authorization :perm` is now a convenience alias for `gate :requires, :perm`. The legacy dual-path in `Tool.permitted?` (one branch for `_permission`, another for gates) is gone; there is one pipeline now. Mirrors the field-level migration done in 0.3.0 (#12), where `@requires` was migrated through the same generic predicate pipeline rather than carrying its own special-cased branch. `_permission` remains exposed as before for introspection.
24
+ - `permitted?(nil_context)` now denies when gates are declared (previously crashed). A nil context reaching `permitted?` is a programmer error; fail-closed avoids silently exposing the tool.
25
+
26
+ ### Migration notes
27
+ - No breaking changes for end users. Tools that use `authorization :perm` continue to work exactly as before — the only difference is the internal pipeline.
28
+ - If you read `tool_class._permission` for introspection, it still returns the declared symbol.
29
+ - Existing field-level `@requires`/`@feature` semantics are unchanged.
30
+
7
31
  ## [0.3.0] - 2026-05-14
8
32
 
9
33
  ### Added
data/README.md CHANGED
@@ -10,11 +10,11 @@ The gem gives you three independent controls over what each user sees:
10
10
 
11
11
  | Layer | Mechanism | Effect |
12
12
  |---|---|---|
13
- | **Tool visibility** | `authorization :manage_workflows` on the tool class | Tool hidden entirely from users who lack the flag |
14
- | **Input fields** | `@requires(:backward_routing)` on a param in `#:` annotation | Field excluded from the input schema *and* stripped from inbound params at call time |
15
- | **Output variants** | `@requires(:backward_routing)` 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 |
13
+ | **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 |
14
+ | **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 |
15
+ | **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 |
16
16
 
17
- All three go through the same predicate: `current_user.can?(:symbol)`. The symbol can represent a permission, a feature flag, a plan tier, an A/B bucket -- whatever your app puts behind it.
17
+ 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](#generic-predicate-tags) below.
18
18
 
19
19
  ### Enforcement, not just shaping
20
20
 
@@ -287,7 +287,9 @@ Shared types define **shapes**. Authorization (`@requires`) stays on the handler
287
287
  ```ruby
288
288
  class MyTool < McpAuthorization::Tool
289
289
  tool_name "my_tool"
290
- authorization :some_flag # tool hidden when can?(:some_flag) is false
290
+ authorization :some_flag # RBAC: hidden unless current_user.can?(:some_flag)
291
+ gate :feature, :order_tracking # any predicate: hidden unless server_context.feature?(:order_tracking)
292
+ gate :tier, :enterprise # multiple gates AND together
291
293
  tags "recruiting", "operations" # which domains this tool appears in
292
294
  read_only! # MCP annotation hints
293
295
  dynamic_contract MyService # handler class
@@ -297,7 +299,8 @@ end
297
299
  | Method | Purpose |
298
300
  |---|---|
299
301
  | `tool_name "name"` | MCP tool name |
300
- | `authorization :sym` | Tool-level visibility gate. Omit for public tools. |
302
+ | `authorization :sym` | Tool-level RBAC visibility gate. Convenience alias for `gate :requires, :sym` — routes through the generic gate pipeline and falls back to `current_user.can?(:sym)` when the server context lacks a `requires?` method. Omit for public tools. |
303
+ | `gate :predicate, :value` | Tool-level generic predicate gate. Calls `server_context.{predicate}?(value)`. Repeat for AND. Fail-open when the predicate method is missing (warning logged in dev). |
301
304
  | `tags "domain1", ...` | Domain(s) this tool appears under. Defaults to `["default"]`. |
302
305
  | `dynamic_contract HandlerClass` | Handler providing description, schemas, and execution |
303
306
  | `read_only!` | Annotation: tool only reads data |
@@ -307,6 +310,8 @@ end
307
310
  | `open_world!` | Annotation: tool may access external services |
308
311
  | `closed_world!` | Annotation: tool stays within the system |
309
312
 
313
+ `authorization :perm` is just a convenience for `gate :requires, :perm` — internally there is one gate pipeline, not two. Multiple `gate` declarations AND together with `authorization`: the tool is shown only when every check passes. This makes tool-level gating symmetric with the field-level annotations (`@requires`, `@feature`, any custom predicate).
314
+
310
315
  Tools self-register when loaded. Put them anywhere under `tool_paths` (default: `app/mcp/`).
311
316
 
312
317
  ## Contract validation
@@ -455,6 +460,26 @@ def beta?(flag) = current_account.beta_enrolled?(flag.to_s)
455
460
 
456
461
  Multiple predicates on the same field are AND-ed — all must pass for the field to appear.
457
462
 
463
+ ### Tool-level gates
464
+
465
+ The same predicate vocabulary is available at the **tool wrapper** level via `gate :predicate, :value`:
466
+
467
+ ```ruby
468
+ class BulkSendSmsTool < McpAuthorization::Tool
469
+ authorization :communications # RBAC permission
470
+ gate :feature, :sms # hide tool unless account has SMS configured
471
+ gate :requires, :super_user # extra RBAC check beyond authorization
472
+ end
473
+ ```
474
+
475
+ `gate` is the tool-level counterpart of `@predicate(:value)` on a field. Semantics:
476
+
477
+ - Calls `server_context.{predicate_name}?(value)` at request time.
478
+ - All gates AND together with `authorization`. The tool is shown only when every check passes.
479
+ - Fail-open when the predicate method is missing on the server context (warning logged in development).
480
+ - `gate :requires, :perm` falls back to `current_user.can?(:perm)` when the context lacks a `requires?` method (matching the field-level backward-compat path).
481
+ - Exceptions raised by a predicate are rescued and logged — a broken predicate never crashes `tools/list`.
482
+
458
483
  **Niche:**
459
484
 
460
485
  | Tag | JSON Schema |
@@ -0,0 +1,84 @@
1
+ module McpAuthorization
2
+ # Shared diagnostic helpers used by both the field-level
3
+ # +RbsSchemaCompiler#predicate_excluded?+ and the tool-level
4
+ # +Tool#gates_pass?+ paths.
5
+ #
6
+ # Lives in its own module so the two predicate sites — field-level
7
+ # (compile time) and tool-level (request time) — share a single
8
+ # "Did you mean?" warning implementation. Avoids two copies of
9
+ # Levenshtein drifting.
10
+ module Diagnostics
11
+ module_function
12
+
13
+ # Emit a development-mode warning when a predicate method is not
14
+ # found on the server context. Helps catch typos like
15
+ # +@feture(:x)+ or +gate :feture, :sms+.
16
+ #
17
+ # @param name [String, Symbol] The predicate name attempted (e.g. +"feature"+ or +:gate+).
18
+ # @param server_context [Object] The context the predicate was looked up on.
19
+ # @param site [Symbol] +:field+ or +:tool+ — controls the suggestion phrasing.
20
+ # @return [void]
21
+ #: (String | Symbol, untyped, Symbol) -> void
22
+ def warn_unknown_predicate(name, server_context, site:)
23
+ return unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env.local?
24
+
25
+ str = name.to_s
26
+ available = server_context.class.public_instance_methods(true)
27
+ .select { |m| m.to_s.end_with?("?") }
28
+ .map { |m| m.to_s.chomp("?") }
29
+ best = available.min_by { |a| levenshtein(a, str) }
30
+ suggestion = best && levenshtein(best, str) <= 3 ? best : nil
31
+
32
+ hint =
33
+ case site
34
+ when :tool then suggestion ? " Did you mean gate :#{suggestion}?" : ""
35
+ when :field then suggestion ? " Did you mean @#{suggestion}?" : ""
36
+ else suggestion ? " Did you mean #{suggestion}?" : ""
37
+ end
38
+
39
+ surface =
40
+ case site
41
+ when :tool then "Gate predicate"
42
+ when :field then "Predicate"
43
+ else "Predicate"
44
+ end
45
+
46
+ fallthrough =
47
+ case site
48
+ when :tool then "Tool will be shown to all users."
49
+ when :field then "Field will be shown to all users."
50
+ else ""
51
+ end
52
+
53
+ Rails.logger&.warn(
54
+ "[McpAuthorization] #{surface} '#{str}?' not found on #{server_context.class}.#{hint} #{fallthrough}".strip
55
+ )
56
+ end
57
+
58
+ # Minimal Levenshtein distance for typo suggestions.
59
+ #
60
+ # Iterative two-row implementation — O(m*n) time, O(m) extra space.
61
+ # Used only in development for "Did you mean?" suggestions, so the
62
+ # naive version is fine.
63
+ #
64
+ # @param a [String]
65
+ # @param b [String]
66
+ # @return [Integer]
67
+ #: (String, String) -> Integer
68
+ def levenshtein(a, b)
69
+ m, n = a.length, b.length
70
+ d = Array.new(m + 1) { |i| i }
71
+ (1..n).each do |j|
72
+ prev = d[0]
73
+ d[0] = j
74
+ (1..m).each do |i|
75
+ cost = a[i - 1] == b[j - 1] ? 0 : 1
76
+ temp = d[i]
77
+ d[i] = [d[i] + 1, d[i - 1] + 1, prev + cost].min
78
+ prev = temp
79
+ end
80
+ end
81
+ d[m]
82
+ end
83
+ end
84
+ end
@@ -1,3 +1,5 @@
1
+ require_relative "diagnostics"
2
+
1
3
  module McpAuthorization
2
4
  # Compiles RBS-style type annotations in Ruby source files into JSON Schema,
3
5
  # with per-request filtering based on +@requires+ permission tags.
@@ -602,35 +604,12 @@ module McpAuthorization
602
604
  end
603
605
 
604
606
  # Emit a development-mode warning when a predicate method is not
605
- # found on the server_context. Helps catch typos like @feture(:x).
607
+ # found on the server_context. Delegates to the shared diagnostic
608
+ # helper so the field-level (+@predicate+) and tool-level (+gate+)
609
+ # warning shapes stay in sync.
606
610
  #: (String, untyped) -> void
607
611
  def warn_unknown_predicate(name, server_context)
608
- return unless defined?(Rails) && Rails.respond_to?(:env) && Rails.env.local?
609
-
610
- available = server_context.class.public_instance_methods(true)
611
- .select { |m| m.to_s.end_with?("?") }
612
- .map { |m| m.to_s.chomp("?") }
613
- best = available.min_by { |a| levenshtein(a, name) }
614
- hint = best && levenshtein(best, name) <= 3 ? " Did you mean @#{best}?" : ""
615
- Rails.logger&.warn("[McpAuthorization] Predicate '#{name}?' not found on #{server_context.class}.#{hint} Field will be shown to all users.")
616
- end
617
-
618
- # Minimal Levenshtein distance for typo suggestions.
619
- #: (String, String) -> Integer
620
- def levenshtein(a, b)
621
- m, n = a.length, b.length
622
- d = Array.new(m + 1) { |i| i }
623
- (1..n).each do |j|
624
- prev = d[0]
625
- d[0] = j
626
- (1..m).each do |i|
627
- cost = a[i - 1] == b[j - 1] ? 0 : 1
628
- temp = d[i]
629
- d[i] = [d[i] + 1, d[i - 1] + 1, prev + cost].min
630
- prev = temp
631
- end
632
- end
633
- d[m]
612
+ McpAuthorization::Diagnostics.warn_unknown_predicate(name, server_context, site: :field)
634
613
  end
635
614
 
636
615
  # Compile a record-style input type (+# @rbs type input = { ... }+)
@@ -1,3 +1,5 @@
1
+ require_relative "diagnostics"
2
+
1
3
  module McpAuthorization
2
4
  # Base class for MCP tools with schema-shaping authorization.
3
5
  #
@@ -10,13 +12,18 @@ module McpAuthorization
10
12
  #
11
13
  # class Tools::ListOrders < McpAuthorization::Tool
12
14
  # tool_name "list_orders"
13
- # authorization :view_orders
15
+ # authorization :view_orders # RBAC permission (legacy, still supported)
16
+ # gate :feature, :order_tracking # generic predicate gate (any predicate name)
14
17
  # tags "operator", "fulfillment"
15
18
  # read_only!
16
19
  #
17
20
  # dynamic_contract Handlers::ListOrders
18
21
  # end
19
22
  #
23
+ # Both +authorization+ and any number of +gate+ declarations contribute to
24
+ # visibility — the tool is shown only when every check passes. See
25
+ # +permitted?+ for the resolution order.
26
+ #
20
27
  class Tool < MCP::Tool
21
28
  class NotAuthorizedError < StandardError; end
22
29
 
@@ -27,6 +34,9 @@ module McpAuthorization
27
34
  #: Array[String]?
28
35
  attr_reader :_tags
29
36
 
37
+ #: Array[Hash[Symbol, untyped]]?
38
+ attr_reader :_gates
39
+
30
40
  #: untyped
31
41
  attr_reader :_contract_handler
32
42
 
@@ -36,10 +46,23 @@ module McpAuthorization
36
46
  McpAuthorization::ToolRegistry.register(subclass)
37
47
  end
38
48
 
39
- # Declare the permission flag required to see this tool.
49
+ # Declare the RBAC permission flag required to see this tool.
50
+ #
51
+ # Convenience alias for +gate :requires, permission+. The generic
52
+ # gate pipeline handles dispatch — calling
53
+ # +server_context.requires?(permission)+ when defined, otherwise
54
+ # falling back to +current_user.can?(permission)+. This mirrors the
55
+ # field-level migration done in 0.3.0 (#12): +@requires+ also went
56
+ # through the generic predicate pipeline rather than carrying its
57
+ # own special-cased branch.
58
+ #
59
+ # +_permission+ remains exposed for introspection — the value is
60
+ # written there as before — but the actual gating goes through the
61
+ # gate list at +permitted?+ time, just like every other check.
40
62
  #: (Symbol) -> void
41
63
  def authorization(permission)
42
64
  @_permission = permission
65
+ gate :requires, permission
43
66
  end
44
67
 
45
68
  # Declare which MCP domains this tool belongs to.
@@ -48,6 +71,30 @@ module McpAuthorization
48
71
  @_tags = list.flatten
49
72
  end
50
73
 
74
+ # Declare a generic predicate gate that must pass for this tool to be
75
+ # visible. The gate calls +server_context.{predicate}?(value)+ at
76
+ # request time. If the predicate returns false, the tool is hidden
77
+ # from +tools/list+ and rejected from +tools/call+.
78
+ #
79
+ # Mirrors the field-level +@predicate(:value)+ system: any predicate
80
+ # name works, as long as the +server_context+ implements
81
+ # +{predicate}?(value)+.
82
+ #
83
+ # class BulkSendSmsTool < McpAuthorization::Tool
84
+ # authorization :communications # RBAC (existing)
85
+ # gate :feature, :sms # hide tool unless account has SMS configured
86
+ # gate :requires, :super_user # extra RBAC check beyond authorization
87
+ # end
88
+ #
89
+ # Multiple +gate+ calls AND together — every gate must pass.
90
+ #
91
+ # @param predicate_name [Symbol] Predicate name; resolved to +{predicate_name}?+ on the context.
92
+ # @param value [Symbol, String] Argument passed to the predicate method.
93
+ #: (Symbol, untyped) -> void
94
+ def gate(predicate_name, value)
95
+ (@_gates ||= []) << { name: predicate_name.to_sym, value: value }
96
+ end
97
+
51
98
  # MCP annotation hint shorthands
52
99
  #: () -> void
53
100
  def read_only!; merge_annotations(read_only_hint: true) end
@@ -94,10 +141,17 @@ module McpAuthorization
94
141
  end
95
142
 
96
143
  # Check whether the current user is allowed to see this tool.
144
+ #
145
+ # Evaluates every declared gate against the server context. A tool
146
+ # is permitted only when every gate passes. With no gates declared,
147
+ # the tool is unconditionally visible.
148
+ #
149
+ # +authorization :perm+ contributes a +gate :requires, :perm+
150
+ # internally, so it goes through the same pipeline as every other
151
+ # predicate. There is one code path for gating, not two.
97
152
  #: (untyped) -> bool
98
153
  def permitted?(server_context)
99
- return true if _permission.nil?
100
- server_context.current_user.can?(_permission)
154
+ gates_pass?(server_context)
101
155
  end
102
156
 
103
157
  # Build the full MCP tool definition hash for +tools/list+.
@@ -187,6 +241,57 @@ module McpAuthorization
187
241
 
188
242
  private
189
243
 
244
+ # Evaluate all declared +gate+ predicates against the server context.
245
+ # Returns true if every gate passes.
246
+ #
247
+ # No gates declared → permissive (tool is public). Gates declared
248
+ # but no server context → deny (a programmer error reaching this
249
+ # method should not silently expose the tool).
250
+ #
251
+ # Symmetric with field-level +RbsSchemaCompiler#predicate_excluded?+:
252
+ # - Unknown predicate (server_context does not implement +{name}?+):
253
+ # fail-open per-gate (gate passes, tool shown). A warning is
254
+ # logged in development environments.
255
+ # - +requires+ predicate without a +requires?+ method: backward-compat
256
+ # fallback to +current_user.can?(value)+.
257
+ # - Predicate raises an exception: fail-open per-gate and log.
258
+ #: (untyped) -> bool
259
+ def gates_pass?(server_context)
260
+ return true if _gates.nil? || _gates.empty?
261
+ return false unless server_context # gates declared + nil context → deny
262
+ _gates.all? { |gate| evaluate_gate(gate, server_context) }
263
+ end
264
+
265
+ #: (Hash[Symbol, untyped], untyped) -> bool
266
+ def evaluate_gate(gate, server_context)
267
+ method = :"#{gate[:name]}?"
268
+ if server_context.respond_to?(method)
269
+ !!server_context.public_send(method, gate[:value])
270
+ elsif gate[:name] == :requires && server_context.respond_to?(:current_user)
271
+ # Backward compat: fall back to direct user permission check.
272
+ # Mirrors RbsSchemaCompiler#predicate_excluded? handling for the
273
+ # OpenStruct-style contexts that predate ServerContext.
274
+ !!server_context.current_user&.can?(gate[:value].to_sym)
275
+ else
276
+ warn_unknown_gate(gate[:name], server_context)
277
+ true # Fail-open: show the tool
278
+ end
279
+ rescue StandardError => e
280
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
281
+ Rails.logger.error("[McpAuthorization] tool gate #{gate[:name]}?(#{gate[:value]}) raised: #{e.message}")
282
+ end
283
+ true # Fail-open on error: show the tool
284
+ end
285
+
286
+ # Emit a development-mode warning when a gate predicate method is not
287
+ # found on the server context. Delegates to the shared diagnostic
288
+ # helper so the field-level (+@predicate+) and tool-level (+gate+)
289
+ # warning shapes stay in sync.
290
+ #: (Symbol, untyped) -> void
291
+ def warn_unknown_gate(name, server_context)
292
+ McpAuthorization::Diagnostics.warn_unknown_predicate(name, server_context, site: :tool)
293
+ end
294
+
190
295
  #: (**untyped) -> void
191
296
  def merge_annotations(**new_hints)
192
297
  hints = (@_annotation_hints || {}).merge(new_hints)
@@ -1,3 +1,3 @@
1
1
  module McpAuthorization
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require "mcp"
2
2
  require_relative "mcp_authorization/version"
3
3
  require_relative "mcp_authorization/configuration"
4
+ require_relative "mcp_authorization/diagnostics"
4
5
  require_relative "mcp_authorization/dsl"
5
6
  require_relative "mcp_authorization/rbs_schema_compiler"
6
7
  require_relative "mcp_authorization/tool_registry"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp_authorization
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - AndyGauge
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-14 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -67,8 +67,9 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '13.0'
69
69
  description: Add MCP tool serving to any Rails app. Write @rbs type annotations with
70
- @requires(:flag) tags and the gem compiles per-user JSON Schema automatically. Feature
71
- flags, permissions, and plan tiers all work through a single can?(:symbol) predicate.
70
+ predicate tags (@requires, @feature, or custom) and the gem compiles per-user JSON
71
+ Schema automatically filtering fields by permissions, feature flags, and plan
72
+ tiers at request time.
72
73
  email:
73
74
  executables: []
74
75
  extensions: []
@@ -80,6 +81,7 @@ files:
80
81
  - app/controllers/mcp_authorization/mcp_controller.rb
81
82
  - lib/mcp_authorization.rb
82
83
  - lib/mcp_authorization/configuration.rb
84
+ - lib/mcp_authorization/diagnostics.rb
83
85
  - lib/mcp_authorization/dsl.rb
84
86
  - lib/mcp_authorization/engine.rb
85
87
  - lib/mcp_authorization/rbs_schema_compiler.rb