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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1dabd038e65fbf04f01e9ae27d109e8734ae78b4db4baeaa6d20200291a3899
4
- data.tar.gz: 0250e2902b73117cfe4447f6036783e4a49613ee4626a407560a82bd19bf25ef
3
+ metadata.gz: '058c192ec678d3ceb11d6ae5fed31c1025156a83d40cf12f7458054f50935682'
4
+ data.tar.gz: 00aee0863c22ead686543f40e8658a3a019711db84c8554c28bcac287bc51253
5
5
  SHA512:
6
- metadata.gz: fbdfb8c28047b39373227ece236670dfc6df6ee34a32ca36f77d6b49f3e31ba7859d82a4c998ff505534ca3e55611f506d7b20a976cd0095a54ce03bc2377a66
7
- data.tar.gz: 6ba9ec325debe3564cca6f8b5fdbee732aab893e1dc744d261fbac53db22772b619b764c1a4d9bb9baff59961d5352b334a9c56e171fda8c44da8e4a5d052a79
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
- return build_facade(domain, category, tools, server_context, config) if facade_name(category, config) == name
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
- #: (String, Symbol, Array[singleton(McpAuthorization::Tool)], untyped, Hash[Symbol, untyped]) -> singleton(MCP::Tool)
114
- def build_facade(domain, category, tools, server_context, config)
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
- meta_payload = strategy == :vendor_extension ? { "tool-input-schemas" => child_schema_map(tools, server_context) } : nil
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. All three keep
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
- #: (Array[singleton(McpAuthorization::Tool)], untyped, Symbol) -> Hash[Symbol, untyped]
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
@@ -1,3 +1,3 @@
1
1
  module McpAuthorization
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.1"
3
3
  end
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.0
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-22 00:00:00.000000000 Z
11
+ date: 2026-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails