mcp_authorization 0.1.0 → 0.2.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 +35 -0
- data/README.md +12 -3
- data/lib/mcp_authorization/rbs_schema_compiler.rb +181 -3
- data/lib/mcp_authorization/tool.rb +40 -3
- data/lib/mcp_authorization/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9adb27c3a4074869c90737b393772a425931683c8b8128864525902e7283aed6
|
|
4
|
+
data.tar.gz: 1d0e2d793b46ff13952142d3e3a21b0a0f6008039e184446506cf90b9e5a1083
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1b46015ca220581022647d667f1d86809c83e433ef5cd2ea500d8b81a2fc2eb9a58346746b74c0a3dbe78209f51728e0b7b2593f5dc7e9fd7663c2fb19638df9
|
|
7
|
+
data.tar.gz: ec9e5d04a5a5d749a995f73026d05bd003e4ab2421fc97d872d67af07bf4fd3f909d1aaa77053c6aca644ffbbbaf89257fc77774bf0800d648ebaf0cb3d428ae
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.2.1] - 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `tools/call` responses now include `structuredContent` when the tool declares an `outputSchema` via `dynamic_contract`. Previously the response carried only text content, which spec-compliant clients rejected as a validation error. (#6)
|
|
11
|
+
- `RbsSchemaCompiler` now finds the handler's source file when `#call` is wrapped via `Module#prepend` (param coercion, instrumentation, `ActiveSupport::Concern`, tracing libraries). It walks `UnboundMethod#super_method` past prepended modules until the owner is the handler class itself, so `# @rbs type` and `#:` annotations are read from the right file instead of raising a contract violation. (#8)
|
|
12
|
+
|
|
13
|
+
## [0.2.0] - 2026-04-20
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `RbsSchemaCompiler.filter_input(handler, params, server_context:)` — projects inbound params onto the user's compiled input schema before the handler runs. Keys gated by `@requires` the user lacks, and any keys not declared in the schema at all, are dropped.
|
|
17
|
+
- `RbsSchemaCompiler.filter_output(handler, result, server_context:)` — projects the handler's return value onto the user's compiled output schema. Hidden `oneOf` variants and their fields are stripped before serialization.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- **`@requires` is now a security boundary, not just a hint to the LLM.** Tool calls through `Tool.call` and the anonymous class produced by `Tool.materialize_for` pipe params through `filter_input` on the way in and results through `filter_output` on the way out. A crafted JSON-RPC request that sends a gated param, and a handler that accidentally emits a gated output field, can no longer leak.
|
|
21
|
+
- README updated to describe enforcement as a guarantee. Handler authors no longer have to remember to re-check `can?` in every branch that touches a gated field — the schema is the boundary.
|
|
22
|
+
|
|
23
|
+
### Migration notes
|
|
24
|
+
- If your handler's `#call` quietly accepted params that weren't declared in the `#:` annotation, those will now arrive as `nil`/default values. Declare them (with `@requires` if appropriate) or drop them.
|
|
25
|
+
- If your handler's output included fields that weren't in `@rbs type output`, those are now stripped. Add them to the output type definition if they should ship.
|
|
26
|
+
|
|
27
|
+
## [0.1.1] - 2026-04-02
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Added MIT license, homepage, author metadata.
|
|
31
|
+
|
|
32
|
+
## [0.1.0] - 2026-04-01
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- Initial gem extraction from the monorepo. Rails engine, `RbsSchemaCompiler`, `Tool` / `ToolRegistry`, `DSL` mixin, `McpController`.
|
data/README.md
CHANGED
|
@@ -11,11 +11,20 @@ The gem gives you three independent controls over what each user sees:
|
|
|
11
11
|
| Layer | Mechanism | Effect |
|
|
12
12
|
|---|---|---|
|
|
13
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 |
|
|
15
|
-
| **Output variants** | `@requires(:backward_routing)` on a variant in `@rbs type output` | Variant excluded from the `oneOf` |
|
|
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 |
|
|
16
16
|
|
|
17
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.
|
|
18
18
|
|
|
19
|
+
### Enforcement, not just shaping
|
|
20
|
+
|
|
21
|
+
`@requires` is a security boundary, not a hint. At tool-call time the gem:
|
|
22
|
+
|
|
23
|
+
- **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 `#call` is invoked. A handler that takes `force:` gated behind `@requires(:admin)` will see `force: false` (its default) for non-admins even if the MCP client sends `force: true` in the raw JSON-RPC payload.
|
|
24
|
+
- **Projects the handler's return value** onto the user's compiled output schema. A variant hidden by `@requires` has 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.
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
19
28
|
## Install
|
|
20
29
|
|
|
21
30
|
```ruby
|
|
@@ -544,7 +553,7 @@ The tradeoff: stateless mode cannot send `notifications/tools/list_changed` or u
|
|
|
544
553
|
## Requirements
|
|
545
554
|
|
|
546
555
|
- Ruby >= 3.1
|
|
547
|
-
- Rails >=
|
|
556
|
+
- Rails >= 6.0
|
|
548
557
|
- [mcp](https://rubygems.org/gems/mcp) ~> 0.10
|
|
549
558
|
|
|
550
559
|
## License
|
|
@@ -81,6 +81,45 @@ module McpAuthorization
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Filter incoming params against the user's compiled input schema.
|
|
85
|
+
#
|
|
86
|
+
# Any key that is not in the schema for this user is dropped — including
|
|
87
|
+
# +@requires+-gated fields the user lacks permission for, and any
|
|
88
|
+
# unknown fields not declared in the schema. This is the runtime
|
|
89
|
+
# enforcement counterpart to the input-shaping that +compile_input+ did.
|
|
90
|
+
#
|
|
91
|
+
# @param handler_class [Class]
|
|
92
|
+
# @param params [Hash] Params as received from the MCP client.
|
|
93
|
+
# @param server_context [Object] Per-request context.
|
|
94
|
+
# @return [Hash] Filtered params safe to pass to the handler.
|
|
95
|
+
#: (untyped, Hash[untyped, untyped], server_context: untyped) -> Hash[untyped, untyped]
|
|
96
|
+
def filter_input(handler_class, params, server_context:)
|
|
97
|
+
return params unless params.is_a?(Hash)
|
|
98
|
+
schema = compile_input_for_filter(handler_class, server_context: server_context)
|
|
99
|
+
return params unless schema
|
|
100
|
+
project_against_schema(params, schema, defs_from(schema))
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Filter the handler's return value against the user's compiled output
|
|
104
|
+
# schema. Any field/variant not visible to this user is stripped from
|
|
105
|
+
# the result.
|
|
106
|
+
#
|
|
107
|
+
# This is the runtime counterpart to +compile_output+: even if a handler
|
|
108
|
+
# bug or auth confusion causes it to emit fields the user shouldn't see,
|
|
109
|
+
# they never cross the wire. Passes the result through unchanged if no
|
|
110
|
+
# +@rbs type output+ is defined.
|
|
111
|
+
#
|
|
112
|
+
# @param handler_class [Class]
|
|
113
|
+
# @param result [Object] Handler return value (hash/array/primitive).
|
|
114
|
+
# @param server_context [Object] Per-request context.
|
|
115
|
+
# @return [Object] Projected result, matching the user's output schema.
|
|
116
|
+
#: (untyped, untyped, server_context: untyped) -> untyped
|
|
117
|
+
def filter_output(handler_class, result, server_context:)
|
|
118
|
+
schema = compile_output_for_filter(handler_class, server_context: server_context)
|
|
119
|
+
return result unless schema
|
|
120
|
+
project_against_schema(result, schema, defs_from(schema))
|
|
121
|
+
end
|
|
122
|
+
|
|
84
123
|
# Strip JSON Schema keywords unsupported by Anthropic's strict tool
|
|
85
124
|
# use mode, and add additionalProperties: false to all objects.
|
|
86
125
|
# Converts oneOf to anyOf (strict mode supports anyOf but not oneOf).
|
|
@@ -152,6 +191,132 @@ module McpAuthorization
|
|
|
152
191
|
|
|
153
192
|
private
|
|
154
193
|
|
|
194
|
+
# ---------------------------------------------------------------
|
|
195
|
+
# Runtime enforcement — project values against the user's schema
|
|
196
|
+
# ---------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
# Like +compile_input+ but keeps +$defs+ inline-resolvable and skips
|
|
199
|
+
# the LLM-facing +strict_sanitize+ pass. Used by +filter_input+ so
|
|
200
|
+
# enforcement operates on the same semantic schema the LLM was given
|
|
201
|
+
# without any strict-mode transforms that would lose type info.
|
|
202
|
+
#: (untyped, server_context: untyped) -> Hash[Symbol, untyped]
|
|
203
|
+
def compile_input_for_filter(handler_class, server_context:)
|
|
204
|
+
cached = cache_for(handler_class)
|
|
205
|
+
|
|
206
|
+
schema = if cached[:raw_input]&.dig(:kind) == :record
|
|
207
|
+
compile_tagged_record(cached[:raw_input][:body], cached[:type_map], server_context)
|
|
208
|
+
else
|
|
209
|
+
build_input_schema(
|
|
210
|
+
filter_call_signature(cached[:call_params], cached[:type_map], server_context)
|
|
211
|
+
)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
with_ref_injection(schema, cached[:type_map])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Like +compile_output+ but skips +strict_sanitize+. Returns nil when
|
|
218
|
+
# the handler has no +# @rbs type output+ declaration.
|
|
219
|
+
#: (untyped, server_context: untyped) -> Hash[Symbol, untyped]?
|
|
220
|
+
def compile_output_for_filter(handler_class, server_context:)
|
|
221
|
+
cached = cache_for(handler_class)
|
|
222
|
+
return nil unless cached[:raw_output]&.dig(:kind) == :union
|
|
223
|
+
|
|
224
|
+
schema = compile_tagged_union(cached[:raw_output][:body], cached[:type_map], server_context)
|
|
225
|
+
with_ref_injection(schema, cached[:type_map])
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Extract the +$defs+ table from a compiled schema for +$ref+ resolution.
|
|
229
|
+
#: (Hash[Symbol, untyped]?) -> Hash[String, Hash[Symbol, untyped]]
|
|
230
|
+
def defs_from(schema)
|
|
231
|
+
return {} unless schema.is_a?(Hash)
|
|
232
|
+
(schema[:"$defs"] || {}).each_with_object({}) { |(k, v), h| h[k.to_s] = v }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# If +schema+ is a +$ref+ pointer, resolve it against +defs+. Otherwise
|
|
236
|
+
# return the schema unchanged.
|
|
237
|
+
#: (Hash[Symbol, untyped]?, Hash[String, Hash[Symbol, untyped]]) -> Hash[Symbol, untyped]?
|
|
238
|
+
def resolve_ref(schema, defs)
|
|
239
|
+
return schema unless schema.is_a?(Hash)
|
|
240
|
+
ref = schema[:"$ref"] || schema["$ref"]
|
|
241
|
+
return schema unless ref
|
|
242
|
+
name = ref.to_s.sub(%r{\A#/\$defs/}, "")
|
|
243
|
+
defs[name] || schema
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Recursively project a runtime value onto a compiled JSON Schema.
|
|
247
|
+
#
|
|
248
|
+
# The semantics: the schema is authoritative — the result only contains
|
|
249
|
+
# what the schema allows for this user.
|
|
250
|
+
#
|
|
251
|
+
# * Objects keep only declared +properties+; everything else is dropped.
|
|
252
|
+
# * Arrays recurse into +items+.
|
|
253
|
+
# * +oneOf+/+anyOf+ picks the best-matching variant (by present/required
|
|
254
|
+
# keys) and projects against it; unmatched variants are ignored.
|
|
255
|
+
# * Primitives (and un-typed schemas) pass through unchanged.
|
|
256
|
+
#
|
|
257
|
+
# This is how +@requires+-gated fields are enforced at runtime: they
|
|
258
|
+
# are already absent from +schema[:properties]+ for a user who lacks
|
|
259
|
+
# the flag, so the projection simply drops them from the value.
|
|
260
|
+
#: (untyped, Hash[Symbol, untyped]?, Hash[String, Hash[Symbol, untyped]]) -> untyped
|
|
261
|
+
def project_against_schema(value, schema, defs)
|
|
262
|
+
return value if schema.nil?
|
|
263
|
+
schema = resolve_ref(schema, defs)
|
|
264
|
+
return value unless schema.is_a?(Hash)
|
|
265
|
+
|
|
266
|
+
variants = schema[:oneOf] || schema[:anyOf]
|
|
267
|
+
if variants.is_a?(Array) && !variants.empty?
|
|
268
|
+
resolved = variants.map { |v| resolve_ref(v, defs) }
|
|
269
|
+
best = best_variant_for(value, resolved)
|
|
270
|
+
return best ? project_against_schema(value, best, defs) : value
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
case schema[:type]
|
|
274
|
+
when "object"
|
|
275
|
+
return value unless value.is_a?(Hash)
|
|
276
|
+
props = schema[:properties] || {}
|
|
277
|
+
value.each_with_object({}) do |(k, v), acc|
|
|
278
|
+
prop_schema = props[k.to_sym] || props[k.to_s] || props[k]
|
|
279
|
+
next unless prop_schema
|
|
280
|
+
acc[k] = project_against_schema(v, prop_schema, defs)
|
|
281
|
+
end
|
|
282
|
+
when "array"
|
|
283
|
+
return value unless value.is_a?(Array)
|
|
284
|
+
items = schema[:items]
|
|
285
|
+
items ? value.map { |v| project_against_schema(v, items, defs) } : value
|
|
286
|
+
else
|
|
287
|
+
value
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Choose the best-matching variant of a union for a given value.
|
|
292
|
+
#
|
|
293
|
+
# Scoring: count how many of the value's keys appear in the variant's
|
|
294
|
+
# +properties+, minus how many are unknown. Disqualify variants missing
|
|
295
|
+
# any of the value's keys from their +required+ list that isn't present.
|
|
296
|
+
# Returns nil if no variant can accommodate the value — in which case
|
|
297
|
+
# the caller should leave the value unchanged (defensive pass-through).
|
|
298
|
+
#: (untyped, Array[Hash[Symbol, untyped]]) -> Hash[Symbol, untyped]?
|
|
299
|
+
def best_variant_for(value, variants)
|
|
300
|
+
return variants.first unless value.is_a?(Hash)
|
|
301
|
+
|
|
302
|
+
scored = variants.filter_map do |variant|
|
|
303
|
+
next unless variant.is_a?(Hash) && variant[:type] == "object"
|
|
304
|
+
props = variant[:properties] || {}
|
|
305
|
+
prop_keys = props.keys.map(&:to_s)
|
|
306
|
+
value_keys = value.keys.map(&:to_s)
|
|
307
|
+
required = (variant[:required] || []).map(&:to_s)
|
|
308
|
+
|
|
309
|
+
next if (required - value_keys).any?
|
|
310
|
+
|
|
311
|
+
known = (value_keys & prop_keys).size
|
|
312
|
+
unknown = (value_keys - prop_keys).size
|
|
313
|
+
[known - unknown, variant]
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
return nil if scored.empty?
|
|
317
|
+
scored.max_by { |score, _| score }&.last
|
|
318
|
+
end
|
|
319
|
+
|
|
155
320
|
# ---------------------------------------------------------------
|
|
156
321
|
# Tag extraction — unified parser for all @tag(...) annotations
|
|
157
322
|
# ---------------------------------------------------------------
|
|
@@ -880,15 +1045,28 @@ module McpAuthorization
|
|
|
880
1045
|
# +Method#source_location+ on its +#call+ method. This is how the
|
|
881
1046
|
# compiler finds the RBS annotations to parse.
|
|
882
1047
|
#
|
|
1048
|
+
# When host applications wrap +#call+ via +prepend+ (param coercion,
|
|
1049
|
+
# instrumentation, error mapping, ActiveSupport::Concern patterns,
|
|
1050
|
+
# observability libraries), +source_location+ on the topmost method
|
|
1051
|
+
# points at the wrapper, not the handler. Walk +super_method+ down
|
|
1052
|
+
# past prepended modules until we find the handler's own definition.
|
|
1053
|
+
#
|
|
883
1054
|
# @param handler_class [Class]
|
|
884
1055
|
# @return [String, nil] Absolute file path, or nil.
|
|
885
1056
|
#: (untyped) -> String?
|
|
886
1057
|
def find_source_file(handler_class)
|
|
887
|
-
if handler_class.method_defined?(:call)
|
|
888
|
-
handler_class.instance_method(:call)
|
|
1058
|
+
um = if handler_class.method_defined?(:call) || handler_class.private_method_defined?(:call)
|
|
1059
|
+
handler_class.instance_method(:call)
|
|
889
1060
|
elsif handler_class.respond_to?(:call)
|
|
890
|
-
handler_class.method(:call)
|
|
1061
|
+
handler_class.method(:call)
|
|
891
1062
|
end
|
|
1063
|
+
return nil unless um
|
|
1064
|
+
|
|
1065
|
+
while um.owner != handler_class && um.super_method
|
|
1066
|
+
um = um.super_method
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
um.source_location&.first
|
|
892
1070
|
end
|
|
893
1071
|
|
|
894
1072
|
# Check whether a string has balanced curly braces. Used to detect
|
|
@@ -118,13 +118,30 @@ module McpAuthorization
|
|
|
118
118
|
end
|
|
119
119
|
|
|
120
120
|
# Execute the tool by delegating to the handler.
|
|
121
|
+
#
|
|
122
|
+
# Inputs are filtered against the user's compiled input schema before
|
|
123
|
+
# being passed to the handler, and outputs are filtered against the
|
|
124
|
+
# user's compiled output schema before being returned. Fields and
|
|
125
|
+
# variants gated by +@requires+ that the user lacks permission for
|
|
126
|
+
# never reach the handler (in) or cross the wire (out).
|
|
121
127
|
#: (?server_context: untyped?, **untyped) -> untyped
|
|
122
128
|
def call(server_context: nil, **params)
|
|
123
129
|
raise NotAuthorizedError unless server_context && permitted?(server_context)
|
|
124
|
-
|
|
130
|
+
filtered = McpAuthorization::RbsSchemaCompiler.filter_input(
|
|
131
|
+
_contract_handler, params, server_context: server_context
|
|
132
|
+
)
|
|
133
|
+
result = handler_instance(server_context).call(**symbolize_keys(filtered))
|
|
134
|
+
McpAuthorization::RbsSchemaCompiler.filter_output(
|
|
135
|
+
_contract_handler, result, server_context: server_context
|
|
136
|
+
)
|
|
125
137
|
end
|
|
126
138
|
|
|
127
139
|
# Create an anonymous MCP::Tool subclass with this user's schemas baked in.
|
|
140
|
+
#
|
|
141
|
+
# The materialized +call+ enforces the compiled schema at runtime:
|
|
142
|
+
# input params are stripped of unknown or permission-gated fields
|
|
143
|
+
# before reaching the handler, and the handler's return value is
|
|
144
|
+
# projected onto the user's output schema before being serialized.
|
|
128
145
|
#: (untyped) -> Class?
|
|
129
146
|
def materialize_for(server_context)
|
|
130
147
|
defn = to_mcp_definition(server_context: server_context)
|
|
@@ -132,6 +149,7 @@ module McpAuthorization
|
|
|
132
149
|
|
|
133
150
|
handler = _contract_handler
|
|
134
151
|
ctx = server_context
|
|
152
|
+
symbolize = method(:symbolize_keys)
|
|
135
153
|
|
|
136
154
|
Class.new(MCP::Tool) do
|
|
137
155
|
tool_name defn[:name]
|
|
@@ -142,12 +160,31 @@ module McpAuthorization
|
|
|
142
160
|
|
|
143
161
|
define_singleton_method(:call) do |server_context: nil, **params|
|
|
144
162
|
effective_ctx = server_context || ctx
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
filtered_params = McpAuthorization::RbsSchemaCompiler.filter_input(
|
|
164
|
+
handler, params, server_context: effective_ctx
|
|
165
|
+
)
|
|
166
|
+
raw = handler.new(server_context: effective_ctx).call(**symbolize.call(filtered_params))
|
|
167
|
+
result = McpAuthorization::RbsSchemaCompiler.filter_output(
|
|
168
|
+
handler, raw, server_context: effective_ctx
|
|
169
|
+
)
|
|
170
|
+
response_args = [{ type: "text", text: result.to_json }]
|
|
171
|
+
if defn[:outputSchema]
|
|
172
|
+
MCP::Tool::Response.new(response_args, structured_content: result)
|
|
173
|
+
else
|
|
174
|
+
MCP::Tool::Response.new(response_args)
|
|
175
|
+
end
|
|
147
176
|
end
|
|
148
177
|
end
|
|
149
178
|
end
|
|
150
179
|
|
|
180
|
+
# Normalize hash keys to symbols so projection output can be splatted
|
|
181
|
+
# into a handler's kwarg-only +#call+ signature.
|
|
182
|
+
#: (Hash[untyped, untyped]) -> Hash[Symbol, untyped]
|
|
183
|
+
def symbolize_keys(hash)
|
|
184
|
+
return {} unless hash.is_a?(Hash)
|
|
185
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
186
|
+
end
|
|
187
|
+
|
|
151
188
|
private
|
|
152
189
|
|
|
153
190
|
#: (**untyped) -> void
|
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.1
|
|
4
|
+
version: 0.2.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-
|
|
11
|
+
date: 2026-05-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -74,6 +74,7 @@ executables: []
|
|
|
74
74
|
extensions: []
|
|
75
75
|
extra_rdoc_files: []
|
|
76
76
|
files:
|
|
77
|
+
- CHANGELOG.md
|
|
77
78
|
- LICENSE
|
|
78
79
|
- README.md
|
|
79
80
|
- app/controllers/mcp_authorization/mcp_controller.rb
|
|
@@ -86,7 +87,7 @@ files:
|
|
|
86
87
|
- lib/mcp_authorization/tool_registry.rb
|
|
87
88
|
- lib/mcp_authorization/version.rb
|
|
88
89
|
- lib/tasks/mcp_authorization.rake
|
|
89
|
-
homepage:
|
|
90
|
+
homepage: https://github.com/onboardiq/mcp_authorization
|
|
90
91
|
licenses:
|
|
91
92
|
- MIT
|
|
92
93
|
metadata: {}
|