mcp_authorization 0.7.0 → 0.7.1
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 +12 -0
- data/lib/mcp_authorization/configuration.rb +9 -0
- data/lib/mcp_authorization/facade_builder.rb +22 -7
- data/lib/mcp_authorization/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '058c192ec678d3ceb11d6ae5fed31c1025156a83d40cf12f7458054f50935682'
|
|
4
|
+
data.tar.gz: 00aee0863c22ead686543f40e8658a3a019711db84c8554c28bcac287bc51253
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aeeb0fa5ab4074654132516b58e0b649aa3eaf3012b3703e98cb9d519c440f39f5bc080bde6947f802a8169073dc03dc1ee2cdf6ffc91b77f520182c7741a143
|
|
7
|
+
data.tar.gz: 89d8566f176bb0c6cb53cc84ca933adb01e858df69caa320d9d480759b7479efd814f6392c25fe61ecfef891c6246b7fbc0c6a349ba9c6892a01f1370c9f6255
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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.7.1]
|
|
8
|
+
|
|
9
|
+
Follow-ups from the 0.7.0 review (onboardiq/mcp_authorization#31).
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- **A facade `tools/call` no longer recompiles every tool in the group.** `FacadeBuilder.facade_for` (the `tools/call` routing path) built the facade with the `:vendor_extension` `_meta` payload, which compiles the input schema of *every* tool in the group — but dispatch only needs the advertised name set, and nothing on the call path reads `_meta`. Over a 30-tool group that was ~30 extra per-tool compiles on every call, partially regressing the 0.6.0 "compile only the invoked tool" optimization on exactly the large domains facades target. `build_facade` now takes `for_dispatch:` (true from `facade_for`) and skips the `_meta` map; `tools/list` (`facades_for`) is unchanged and still carries it.
|
|
13
|
+
- **`facet_domain(group_by:)` is now validated.** Any value but `:category` (the only supported grouping key) raises `ArgumentError`, consistent with `schema_strategy:`/`uncategorized:`. Previously a typo (`group_by: :tag`) was silently accepted and behaved as `:category`.
|
|
14
|
+
- **Corrected a stale RBS annotation on `FacadeBuilder.facade_input_schema`** (declared 3 params for a 2-param method; the intervening prose comment also caused Sentinel to omit it from the generated sigs). Annotation fixed and moved adjacent to the def so it regenerates.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **Cache-vocabulary test for the facade path** (design doc §8): asserts a cold compile of a faceted domain learns the consulted decisions, so two callers with different permission sets produce different `tools_list_key`s (guarding against a shared-listing RBAC leak).
|
|
18
|
+
|
|
7
19
|
## [0.7.0] - 2026-07-21
|
|
8
20
|
|
|
9
21
|
Declarative tool grouping: a domain can present its tools as a small set of
|
|
@@ -132,6 +132,11 @@ module McpAuthorization
|
|
|
132
132
|
# Behaviors for a tool in a faceted domain that declares no +category+.
|
|
133
133
|
UNCATEGORIZED_MODES = %i[fallback error].freeze #: Array[Symbol]
|
|
134
134
|
|
|
135
|
+
# Grouping keys +facet_domain+ knows how to group by. Only +:category+
|
|
136
|
+
# exists today (the +category+ DSL is the only grouping key); accepted
|
|
137
|
+
# explicitly so a future key is an additive, validated change.
|
|
138
|
+
GROUP_BY_KEYS = %i[category].freeze #: Array[Symbol]
|
|
139
|
+
|
|
135
140
|
# Default suffix appended to a category to form its facade tool name
|
|
136
141
|
# (e.g. category +:orders+ → +orders_tools+). Overridable per domain via
|
|
137
142
|
# +facet_domain(..., facade_suffix:)+.
|
|
@@ -186,6 +191,10 @@ module McpAuthorization
|
|
|
186
191
|
# (+[a-z0-9_]+) so the derived name stays a valid MCP tool name.
|
|
187
192
|
#: (Symbol | String, group_by: Symbol, ?schema_strategy: Symbol, ?uncategorized: Symbol, ?facade_suffix: String | Symbol) -> void
|
|
188
193
|
def facet_domain(domain, group_by:, schema_strategy: :vendor_extension, uncategorized: :fallback, facade_suffix: DEFAULT_FACADE_SUFFIX)
|
|
194
|
+
unless GROUP_BY_KEYS.include?(group_by.to_sym)
|
|
195
|
+
raise ArgumentError, "unknown group_by #{group_by.inspect}; " \
|
|
196
|
+
"expected one of #{GROUP_BY_KEYS.inspect}"
|
|
197
|
+
end
|
|
189
198
|
unless SCHEMA_STRATEGIES.include?(schema_strategy)
|
|
190
199
|
raise ArgumentError, "unknown schema_strategy #{schema_strategy.inspect}; " \
|
|
191
200
|
"expected one of #{SCHEMA_STRATEGIES.inspect}"
|
|
@@ -45,13 +45,22 @@ module McpAuthorization
|
|
|
45
45
|
# A single facade by its tool name (e.g. "orders_tools"), or nil when
|
|
46
46
|
# the name matches no non-empty group for this caller. Used by
|
|
47
47
|
# +tools/call+ routing so one call doesn't build every facade.
|
|
48
|
+
#
|
|
49
|
+
# Built with +for_dispatch: true+ — a +tools/call+ only needs the
|
|
50
|
+
# advertised name set to route through, never the +_meta+ per-tool
|
|
51
|
+
# schemas (those are a +tools/list+ concern). Skipping them avoids
|
|
52
|
+
# recompiling every tool's input schema in the group on each call,
|
|
53
|
+
# preserving the "compile only the invoked tool" cost that facades'
|
|
54
|
+
# large target domains most depend on.
|
|
48
55
|
#: (domain: String, name: String, server_context: untyped) -> singleton(MCP::Tool)?
|
|
49
56
|
def facade_for(domain:, name:, server_context:)
|
|
50
57
|
config = McpAuthorization.config.facet_config(domain)
|
|
51
58
|
return nil unless config
|
|
52
59
|
|
|
53
60
|
group_tools(domain, server_context, config).each do |category, tools|
|
|
54
|
-
|
|
61
|
+
if facade_name(category, config) == name
|
|
62
|
+
return build_facade(domain, category, tools, server_context, config, for_dispatch: true)
|
|
63
|
+
end
|
|
55
64
|
end
|
|
56
65
|
nil
|
|
57
66
|
end
|
|
@@ -110,8 +119,12 @@ module McpAuthorization
|
|
|
110
119
|
# Build the synthetic MCP::Tool subclass for one group, with this
|
|
111
120
|
# caller's description, schema, and dispatch baked in — the same
|
|
112
121
|
# materialization shape Tool.materialize_for uses for real tools.
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
#
|
|
123
|
+
# +for_dispatch:+ true skips the +_meta+ per-tool schema map, which only
|
|
124
|
+
# a +tools/list+ consumer reads — a +tools/call+ routes on the advertised
|
|
125
|
+
# name set alone. See +facade_for+.
|
|
126
|
+
#: (String, Symbol, Array[singleton(McpAuthorization::Tool)], untyped, Hash[Symbol, untyped], ?for_dispatch: bool) -> singleton(MCP::Tool)
|
|
127
|
+
def build_facade(domain, category, tools, server_context, config, for_dispatch: false)
|
|
115
128
|
name = facade_name(category, config)
|
|
116
129
|
desc = facade_description(category, tools, server_context)
|
|
117
130
|
strategy = config[:schema_strategy]
|
|
@@ -124,8 +137,9 @@ module McpAuthorization
|
|
|
124
137
|
# keywords, and `_meta` is never forwarded to the model as the tool's
|
|
125
138
|
# input_schema. So the listing stays valid for strict Zod clients and
|
|
126
139
|
# strict-mode LLM tool-calling while still carrying the schemas in-band
|
|
127
|
-
# for capable clients.
|
|
128
|
-
|
|
140
|
+
# for capable clients. Skipped on the dispatch path (nothing reads it
|
|
141
|
+
# there) so a facade call doesn't recompile every tool in the group.
|
|
142
|
+
meta_payload = !for_dispatch && strategy == :vendor_extension ? { "tool-input-schemas" => child_schema_map(tools, server_context) } : nil
|
|
129
143
|
|
|
130
144
|
advertised = tools.map(&:tool_name).to_set
|
|
131
145
|
builder = self
|
|
@@ -166,10 +180,10 @@ module McpAuthorization
|
|
|
166
180
|
DESC
|
|
167
181
|
end
|
|
168
182
|
|
|
169
|
-
# The facade inputSchema for the configured strategy.
|
|
183
|
+
# The facade inputSchema for the configured strategy. Both strategies keep
|
|
170
184
|
# per-tool schemas out of the description and advertise only tools the
|
|
171
185
|
# caller may invoke; they differ in where argument schemas live.
|
|
172
|
-
|
|
186
|
+
#
|
|
173
187
|
# The facade inputSchema is always a flat object — a `tool_name` enum plus
|
|
174
188
|
# a permissive `arguments` object, with NO top-level combinator. LLM tool
|
|
175
189
|
# `input_schema` must have an object root: Anthropic and OpenAI reject
|
|
@@ -180,6 +194,7 @@ module McpAuthorization
|
|
|
180
194
|
# on the facade's `_meta` (:vendor_extension, see build_facade) for a
|
|
181
195
|
# client to expand, or are omitted entirely (:lazy) and enforced at
|
|
182
196
|
# dispatch by the target tool's own filter_input.
|
|
197
|
+
#: (Array[singleton(McpAuthorization::Tool)], Symbol) -> Hash[Symbol, untyped]
|
|
183
198
|
def facade_input_schema(tools, strategy)
|
|
184
199
|
arguments_description =
|
|
185
200
|
if strategy == :vendor_extension
|
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.7.
|
|
4
|
+
version: 0.7.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AndyGauge
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|