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 +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +31 -6
- data/lib/mcp_authorization/diagnostics.rb +84 -0
- data/lib/mcp_authorization/rbs_schema_compiler.rb +6 -27
- data/lib/mcp_authorization/tool.rb +109 -4
- data/lib/mcp_authorization/version.rb +1 -1
- data/lib/mcp_authorization.rb +1 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9042f13cf2b294336b81ade0d8f97c31179a6483dde8c73c2258a7459227c7c4
|
|
4
|
+
data.tar.gz: 8be4296941e757f985bdab130472aa3f7f1bda837eae5d3b8d924078e9f27bb9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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 #
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
data/lib/mcp_authorization.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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
|
|
71
|
-
|
|
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
|