mcp_toolkit 0.6.1 → 0.6.2

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: af5e847538e7fe188e7fb0b0b509f0f430d2c3d33c84fa21350e30367a409578
4
- data.tar.gz: dc3233698fd64947b42330fe51bd230e249f37e9044d303eaf1915ea213e1695
3
+ metadata.gz: fa488bbd5b90e15c5f7e9a70f8792be1ec070b0fdd6ce6e89f1279bd24bf59cf
4
+ data.tar.gz: 2e8953c00c9b6dab828efcd9c92f71990d8e09477f15851aa9976f0b942456ae
5
5
  SHA512:
6
- metadata.gz: a1fc3088f0a6532ed7a52e7cf13e13ce372edbdcddd61f255fd93cb28df6c0c5fe3a3c17ef98cedf8c5a3be5f8c72d58f8f9bdbc28eac1e7b5ae3308a83cd21f
7
- data.tar.gz: fb9a05e81f3895dc89d65dc45a33deef371387d8ba14bf223454f666b8bdeb9c828876a66ba9f6c54b19019b416cd88dc6c66dd5ab562fdc577d1b5148872aae
6
+ metadata.gz: 1f478ffa9b25834f25e4481b964015e8a73719525109986cb59972b6acecd27f9b47c7238ad435f8c30c491c88fee8631b0cd174bd68b4b61440a7996161d05a
7
+ data.tar.gz: 887ada2ad7fd66ba42feae592e8cc2ede5e2e89a075a1d2581d78e93bc8a0cd0bdb0bb7e5ffcb487efa22944ce51d303fa236f146b291f0987a242c4f4ddc6a7
data/CHANGELOG.md CHANGED
@@ -1,3 +1,43 @@
1
+ ## [0.6.2] - 2026-08-28
2
+
3
+ RFC 9207 authorization server issuer identification. Additive hardening, and the
4
+ fix for hosted clients that choose their redirect URI based on whether the
5
+ authorization server supports it.
6
+
7
+ ### Added
8
+
9
+ - **`iss` in the authorization response, and `authorization_response_iss_parameter_supported`
10
+ in the metadata** (RFC 9207). The parameter names which authorization server
11
+ produced a response, so a client registered with several cannot be induced to
12
+ redeem a code at the wrong one — the mixed-up authorization server attack.
13
+
14
+ This also unblocks **ChatGPT connectors**, which pick their `redirect_uri` from
15
+ whether the server meets RFC 9207: when it does, ChatGPT uses the stable
16
+ `https://chatgpt.com/connector_platform_oauth_redirect`; when it does not, it
17
+ mints a per-connector `https://chatgpt.com/connector/oauth/{callback_id}` that
18
+ no exact-match allowlist can express, and the connection fails with an
19
+ unregistered `redirect_uri`. Observed in production against a real customer
20
+ before this release.
21
+
22
+ Note for hosts: a client that picks its callback at *connector-creation* time
23
+ reads the metadata then. An existing connector created against a pre-0.6.2
24
+ server keeps the URI it already chose — **it has to be re-created** to pick up
25
+ the change.
26
+
27
+ `iss` is emitted on the one response this bridge redirects (`approve`); every
28
+ error path renders rather than redirecting, so there is no error response for it
29
+ to be absent from. It is byte-identical to the advertised `issuer` — clients
30
+ compare by exact string and do not normalise trailing slashes, paths, ports or
31
+ casing — and a spec pins the redirect against the discovery document rather than
32
+ against a literal.
33
+
34
+ `iss` is now response-owned alongside `code` and `state`: a caller that seeds one
35
+ into its own `redirect_uri` has it replaced, not appended. A value the caller
36
+ chose would defeat the point of the parameter.
37
+
38
+ **No control was relaxed.** The redirect allowlist is unchanged and still exact-match
39
+ (RFC 9700 §2.1); this release makes conforming clients ask for a URI already on it.
40
+
1
41
  ## [0.6.1] - 2026-07-20
2
42
 
3
43
  Two additive fixes for hosted MCP clients whose OAuth setup could not complete
data/README.md CHANGED
@@ -244,6 +244,98 @@ discovery tool, a custom serializer may also expose `declared_attributes` /
244
244
 
245
245
  ---
246
246
 
247
+ ## Reading data: pagination, sparse fieldsets, filters
248
+
249
+ The four generic tools serve the same grammar on both the satellite and authority
250
+ paths (they share the executors). `list` accepts:
251
+
252
+ | Argument | Shape | Notes |
253
+ |---|---|---|
254
+ | `resource` | String | the registered resource name |
255
+ | `limit` / `offset` | Integer | page size (default 25, max 100) / offset (default 0) |
256
+ | `fields` | Array or comma-separated String | sparse fieldset — attribute and/or relationship names, one flat namespace. Unknown names raise `InvalidParams` rather than being silently dropped |
257
+ | `filter` | Object | per-attribute filters, applied **on top of** the account scope (they can only narrow, never widen) |
258
+ | *(resource-specific)* | — | a resource's own `filter` declarations arrive as **top-level** arguments, not inside `filter` |
259
+
260
+ `list` returns `{ "<resource>": [...], "meta": { total_count, limit, offset } }`.
261
+
262
+ Clients discover all of this at runtime: `resources` lists each resource with
263
+ `filterable` and its usage `note`, and `resource_schema` advertises every
264
+ attribute's type and accepted `operators`, the valid `fields` values, the
265
+ resource's own `resource_filters`, and any companion-key requirements.
266
+
267
+ ### Filter values
268
+
269
+ A filter value is either a **bare value** or an `{ op:, value: }` condition.
270
+
271
+ ```jsonc
272
+ { "filter": { "status": "active" } } // equality
273
+ { "filter": { "status": "active,archived" } } // IN set (comma-separated)
274
+ { "filter": { "status": ["active", "archived"] } } // IN set (array)
275
+ { "filter": { "archived_at": "null" } } // IS NULL ("null" token, or a JSON null)
276
+ { "filter": { "created_at": { "op": "gteq", "value": "2026-01-01" } } }
277
+ { "filter": { "created_at": [ // conditions AND together
278
+ { "op": "gteq", "value": "2026-01-01" },
279
+ { "op": "lt", "value": "2026-02-01" }
280
+ ] } }
281
+ ```
282
+
283
+ Under the default `:tokenized` semantics a bare `""` means "no filter", and a
284
+ comma splits an IN set. Set `bare_filter_value_semantics = :literal` to match
285
+ bare values verbatim instead; operator conditions behave identically in both.
286
+
287
+ ### Operators by column type
288
+
289
+ `resource_schema` advertises these per attribute; `filter_operator_overrides`
290
+ narrows them per type.
291
+
292
+ | Column type | Operators |
293
+ |---|---|
294
+ | `integer` / `float` / `decimal` / `datetime` | `eq` `not_eq` `gt` `gteq` `lt` `lteq` |
295
+ | `date` | `eq` `not_eq` `gt` `gteq` `lt` `lteq` `in` |
296
+ | `string` / `text` | `eq` `in` `not_eq` `matches` `does_not_match` |
297
+ | `boolean` | `eq` `not_eq` |
298
+ | anything else (`uuid`, `enum`, `jsonb`, `citext`, …) | `eq` `in` |
299
+
300
+ `matches` / `does_not_match` are SQL `LIKE`, with wildcards in the value escaped
301
+ by `config.sql_sanitizer`. Only `eq` / `in` / `not_eq` accept a null (`IS NULL` /
302
+ `IS NOT NULL`); a comparison or `LIKE` against null raises `InvalidParams`,
303
+ because it could never match a row. IN-set elements must be non-null scalars —
304
+ SQL `IN` cannot match NULL, so a null-or-nothing condition is expressed as the
305
+ filter's single scalar value.
306
+
307
+ These refusals are deliberate: each previously returned a silently wrong or empty
308
+ result, which is far harder for a client to notice than an error.
309
+
310
+ ### Resource-specific filters and companion keys
311
+
312
+ `filterable` maps public filter keys onto backing columns. When the generic
313
+ equality/operator grammar cannot express a filter, declare a `filter` block —
314
+ it takes a **top-level** request param and narrows the relation itself:
315
+
316
+ ```ruby
317
+ filterable status: :status, owner_id: :owner_id
318
+
319
+ filter :for_project, type: :integer, description: "Only widgets in this project" do |relation, id|
320
+ relation.joins(:board).where(boards: { project_id: id })
321
+ end
322
+ ```
323
+
324
+ `filter_requirements` declares that a key is meaningless alone — a polymorphic
325
+ foreign key is type-ambiguous without its `*_type`, so filtering on it alone
326
+ would silently match rows across types:
327
+
328
+ ```ruby
329
+ filter_requirements subject_id: :subject_type
330
+ ```
331
+
332
+ `list` then rejects `subject_id` unless `subject_type` comes with it, and
333
+ `resource_schema` advertises the requirement under the relationship's
334
+ `filter.requires` so a client can satisfy it without guessing. Both accept a Hash
335
+ or a lazily-resolved callable.
336
+
337
+ ---
338
+
247
339
  ## Configuration reference
248
340
 
249
341
  | Setting | Default | Purpose |
@@ -262,9 +354,11 @@ discovery tool, a custom serializer may also expose `declared_attributes` /
262
354
  | `session_ttl` | `3600` | session sliding TTL (s) |
263
355
  | `protocol_version` | `nil` (negotiate) | pin an MCP protocol version (satellite/upstream client) |
264
356
  | `supported_protocol_versions` | `Protocol::SUPPORTED_VERSIONS` | version set the authority dispatcher negotiates |
265
- | `tool_provider` | `nil` | authority: the host's api-agnostic tool catalog (see below) |
357
+ | `tool_provider` | composed (see below) | authority: the host's api-agnostic tool catalog. Left **unset it composes itself** — the generic `RegistryToolProvider` (only when resources are registered) followed by every `extra_tool_providers` entry — so the common case needs no provider plumbing. Assign explicitly to take full control |
358
+ | `extra_tool_providers` | `[]` | authority: extra providers (or bare tool **classes**, auto-wrapped in a `SingleToolProvider`) composed after the generic tools when `tool_provider` is unset |
266
359
  | `generic_tool_name_prefix` | `""` | authority: prefix namespacing the four generic Registry-backed tools (e.g. `"foo_"` → `foo_resources` …) |
267
360
  | `rate_limiter` / `usage_recorder` / `usage_flusher` | `nil` | authority transport billing hooks (config callables) |
361
+ | `session_data_builder` | `nil` | authority: builds the opaque `Session#data` payload (e.g. bind a session to a token id so revoking the token kills it) |
268
362
  | `rate_limit_max_requests` | `nil` (off) | authority: per-principal request cap for the built-in `RateLimiter`; `nil` disables rate limiting |
269
363
  | `rate_limit_window` | `3600` | authority: fixed rate-limit window (s); ignored while `rate_limit_max_requests` is `nil` |
270
364
  | `superuser_resolver` | `nil` | optional `->(principal) -> Boolean` for `Context#superuser?`; `nil` = duck-type `principal.superuser?` |
@@ -276,13 +370,48 @@ discovery tool, a custom serializer may also expose `declared_attributes` /
276
370
  | `upstream_list_ttl` | `900` | gateway: TTL (s) for an upstream's cached tool list |
277
371
  | `logger` | `nil` | optional logger for gateway/session diagnostics (`Rails.logger`) |
278
372
 
373
+ ### Data path, filtering, and safety caps
374
+
375
+ These govern how `list` reads a filter and how much work one request may ask for.
376
+ The defaults are the gem's own grammar; the first three exist so a host migrating
377
+ an **existing** MCP endpoint onto the gem can preserve its pre-gem contract
378
+ byte-for-byte (see [Migrating an existing endpoint](#migrating-an-existing-mcp-endpoint)).
379
+
380
+ | Setting | Default | Purpose |
381
+ |---|---|---|
382
+ | `bare_filter_value_semantics` | `:tokenized` | how a **bare** filter value is read. `:tokenized` applies the comma/IN/`"null"` grammar below; `:literal` sends the value to the WHERE clause verbatim (`"a,b"` is one string, `"null"` is the literal string). Operator conditions are identical either way |
383
+ | `non_numeric_pk_order` | `:created_at` | ordering for non-numeric-PK resources. `:created_at` (with the PK as tiebreaker, so offset pagination is a total order) or `:primary_key` to preserve an `ORDER BY id` contract |
384
+ | `filter_operator_overrides` | `{}` | per-column-type overrides of the advertised **and** enforced operator sets, e.g. `{ text: %w[eq in], date: %w[eq in] }`. Single source, so `resource_schema` and the executor cannot disagree. Rejects, at assignment, any operator outside `Filtering::AREL_PREDICATIONS` |
385
+ | `max_filter_values` | `500` | caps how many values one IN-set may resolve to, and how many operator conditions may be ANDed on one attribute, so a valid token cannot emit an unbounded IN clause / AND-chain. `nil` disables |
386
+ | `max_batch_size` | `50` | authority: caps the JSON-RPC calls one POST batch may carry. Rate limiting is per-HTTP-request, so an uncapped batch would fan out unbounded work under a single tick. `nil` disables |
387
+ | `sql_sanitizer` | `McpToolkit::SqlSanitizer` | escapes LIKE wildcards in `matches` / `does_not_match`; injectable so a non-Rails host can supply its own |
388
+
389
+ ### OAuth bridge (authority-only, opt-in)
390
+
391
+ All inert unless the bridge is switched on — see
392
+ [OAuth authorization bridge](#oauth-authorization-bridge-authority-only-opt-in)
393
+ for what it is and why the redirect policy is shaped the way it is.
394
+
395
+ | Setting | Default | Purpose |
396
+ |---|---|---|
397
+ | `oauth_allowed_redirect_uris` | `[]` | exact-string allowlist of redirect targets. Validated at assignment (an unparseable, scheme-less, fragment-bearing or opaque URI raises, as does cleartext `http://` to a remote host and the `javascript:`/`data:`/`file:` schemes) and **frozen** once assigned |
398
+ | `oauth_allow_loopback_redirects` | `false` | accept `http://127.0.0.1:*` / `localhost` / `[::1]` without an allowlist entry (RFC 8252 §7.3 — the client picks an ephemeral port, so no list could name it) |
399
+ | `oauth_resource_path` | `"/mcp"` | must match the engine's mount point; `"/"` when the MCP endpoint IS the origin root |
400
+ | `oauth_authorization_code_ttl` | `60` | authorization-code lifetime (s) |
401
+ | `oauth_signing_secret` | Rails' `secret_key_base` | mixed into the key that seals a code's cache entry, so the cache, the logs and the code together still open nothing. Validated at assignment |
402
+ | `oauth_parent_controller` | `"ActionController::Base"` | superclass of the bridge's controller, deliberately **separate** from `parent_controller` — the authorization page is HTML and `ActionController::API` cannot render it |
403
+
404
+ Either naming a redirect target or enabling loopback is what flips
405
+ `config.oauth_bridge?` on; with neither, no route is drawn.
406
+
279
407
  ## Public API surface
280
408
 
281
409
  - `McpToolkit.configure { |c| ... }`, `McpToolkit.config`, `McpToolkit.registry`,
282
410
  `McpToolkit.reset_config!`
283
411
  - `McpToolkit::Registry#register(name) { ... }` (DSL: `model`, `serializer`,
284
412
  `scope`, `description`, `note`, `filterable`, `filter(name, type:, description:,
285
- &applier)`, `superusers_only!`, `required_permissions_scope`) +
413
+ &applier)`, `filter_requirements`, `superusers_only!`,
414
+ `required_permissions_scope`, `extra(key, value)` for host-defined metadata) +
286
415
  `#default_required_permissions_scope`
287
416
  - `McpToolkit::Serializer::Base` (DSL: `attributes`, `has_one`, `has_many`,
288
417
  `translates`)
@@ -342,6 +471,18 @@ McpToolkit.configure do |c|
342
471
  end
343
472
  ```
344
473
 
474
+ Declaring the whole set from ENV has two gotchas every gateway host rediscovers —
475
+ re-registering on a code reload duplicates entries, and a blank ENV var must not
476
+ become an upstream. `register_upstreams_from_env` handles both (it resets the
477
+ registry first, so it is idempotent, and skips blank urls):
478
+
479
+ ```ruby
480
+ c.register_upstreams_from_env(
481
+ "notifications" => "NOTIFICATIONS_SERVER_URL",
482
+ "billing" => "BILLING_SERVER_URL"
483
+ )
484
+ ```
485
+
345
486
  ### Aggregate upstream tool lists
346
487
 
347
488
  `Aggregator#tool_definitions` returns every upstream's tools, namespaced, pulled
@@ -493,6 +634,25 @@ So **every target must be named by exact string**, with exactly one exception:
493
634
  | Private-use scheme (`cursor://…`, `com.example.app:/cb`) | Exact string, in `oauth_allowed_redirect_uris` | Keeps the code on the device, but its URI is a fixed string — so just name it. |
494
635
  | Loopback (`http://127.0.0.1:*`, `localhost`, `[::1]`) | `oauth_allow_loopback_redirects` | The only target that **cannot** be named: the client picks an ephemeral port at runtime (RFC 8252 §7.3). And it resolves on the operator's own machine, so the attack above cannot reach it. |
495
636
 
637
+ ### If a client's callback looks impossible to name, check RFC 9207 first
638
+
639
+ Some hosted clients mint a **per-connector** callback (`https://vendor.example/connector/oauth/{id}`),
640
+ which no exact-match list can express — and the obvious response, matching the
641
+ host and path by pattern, is the wrong one. A prefix turns an attacker into
642
+ someone who can **name their own destination inside it**: they create their own
643
+ connector at that vendor, put their callback in the authorize URL, and the code
644
+ goes somewhere they control. Exact matching leaves a weaker residual (a code sent
645
+ to the vendor's *legitimate* shared callback, separated by the client binding
646
+ `state` to the initiating session — RFC 6819 §4.4.1.7); a prefix removes the need
647
+ for any of that to go wrong.
648
+
649
+ At least one such client picks the per-connector form **only when the
650
+ authorization server does not implement RFC 9207**, and uses a single stable
651
+ callback when it does. Since 0.6.2 this gem implements it, so the fix for that
652
+ class of failure is to be conforming, not to be permissive. Before relaxing the
653
+ policy for a client that "cannot be named", check whether it is asking you for a
654
+ capability instead.
655
+
496
656
  The loopback exception exists because an allowlist entry is *impossible* there,
497
657
  not because native clients are trusted. A private-use scheme keeps the code on the
498
658
  device too, but nothing forces it to be unnamed — and whole **schemes** cannot be
@@ -660,12 +820,18 @@ McpToolkit.configure do |c|
660
820
  note "Read-only projection; do not interpret status codes without domain context."
661
821
  scope { |account| Widget.where(account_id: account.id) }
662
822
  end
663
-
664
- # The generic tools, served over config.registry:
665
- c.tool_provider = McpToolkit::Authority::RegistryToolProvider.new(config: c)
666
823
  end
667
824
  ```
668
825
 
826
+ That is the whole setup — **no `tool_provider` assignment is needed.** Left unset,
827
+ it composes itself from the registry (plus any `extra_tool_providers`), so
828
+ registering resources is enough to serve the generic tools. Assign one explicitly
829
+ only to take full control of the catalog:
830
+
831
+ ```ruby
832
+ c.tool_provider = McpToolkit::Authority::RegistryToolProvider.new(config: c)
833
+ ```
834
+
669
835
  Each generic tool resolves the `resource` argument against the registry, refuses a
670
836
  `superusers_only!` resource for a non-superuser (and hides it from `resources`),
671
837
  enforces the resource's `required_permissions_scope`, and requires a resolved
@@ -685,8 +851,16 @@ c.generic_tool_name_prefix = "foo_" # advertised + resolved as foo_resources,
685
851
  The prefix applies only to these four generic tools; a composed bespoke provider's
686
852
  own tool names are unaffected.
687
853
 
688
- To serve the generic tools **and** your own bespoke tools behind one provider,
689
- compose them:
854
+ To serve the generic tools **and** your own bespoke tools, just name the extras —
855
+ they are composed after the generic ones, and a bare tool **class** is wrapped for
856
+ you:
857
+
858
+ ```ruby
859
+ c.extra_tool_providers = [MyApp::Tools::AuditLog] # a class, or a provider object
860
+ ```
861
+
862
+ Compose by hand only when you want to control the order or drop the generic tools
863
+ entirely:
690
864
 
691
865
  ```ruby
692
866
  c.tool_provider = McpToolkit::Authority::CompositeToolProvider.new(
@@ -744,6 +918,27 @@ it to gate `superusers_only!` resources; with no resolver it duck-types
744
918
  Point your `POST /mcp` route at the subclass (or mount the engine for a pure host);
745
919
  keep `POST /mcp/tokens/introspect` on the gem's `TokensController`.
746
920
 
921
+ ### Migrating an existing MCP endpoint
922
+
923
+ If you are moving an MCP endpoint you already ship onto the gem, your clients
924
+ hold the *old* contract. Several seams exist purely so that contract survives the
925
+ move — adopt them at first, then retire them deliberately rather than breaking
926
+ clients on cutover:
927
+
928
+ | If your endpoint… | Set |
929
+ |---|---|
930
+ | matched bare filter values verbatim (no comma/`"null"` grammar) | `bare_filter_value_semantics = :literal` |
931
+ | ordered non-numeric-PK lists by `id` | `non_numeric_pk_order = :primary_key` |
932
+ | advertised a narrower operator set | `filter_operator_overrides`, e.g. `{ text: %w[eq in], date: %w[eq in] }` |
933
+ | namespaced its generic tool names | `generic_tool_name_prefix` |
934
+ | filtered a polymorphic FK safely | `filter_requirements` on the resource |
935
+
936
+ One deliberate delta is **not** revertible: `{ op: "in", value: "a,b" }` now
937
+ splits into an IN set (previously only `eq` split, and `in` matched the literal
938
+ string `'a,b'`). Under the tokenized operator grammar there is no way to express
939
+ a literal comma inside an IN element — express such a match as a bare equality
940
+ value, which `:literal` semantics match verbatim.
941
+
747
942
  ### Lazy `parent_controller`
748
943
 
749
944
  The gem's controllers subclass `config.parent_controller`. That parent is read
@@ -31,7 +31,7 @@ module McpToolkit::Oauth::ControllerMethods
31
31
 
32
32
  # Query parameters the callback response owns: whatever a client put in its own
33
33
  # redirect_uri, these are set by the redirect and not carried over from it.
34
- RESPONSE_OWNED_QUERY_KEYS = %w[code state].freeze
34
+ RESPONSE_OWNED_QUERY_KEYS = %w[code state iss].freeze
35
35
 
36
36
  # RFC 7636 §4.1: 43–128 unreserved characters. The challenge is §4.2's
37
37
  # base64url of a SHA-256, which is always exactly 43 of the same alphabet.
@@ -95,7 +95,8 @@ module McpToolkit::Oauth::ControllerMethods
95
95
  response_types_supported: SUPPORTED_RESPONSE_TYPES,
96
96
  grant_types_supported: SUPPORTED_GRANT_TYPES,
97
97
  code_challenge_methods_supported: ["S256"],
98
- token_endpoint_auth_methods_supported: ["none"]
98
+ token_endpoint_auth_methods_supported: ["none"],
99
+ authorization_response_iss_parameter_supported: true
99
100
  }
100
101
  end
101
102
 
@@ -408,6 +409,7 @@ module McpToolkit::Oauth::ControllerMethods
408
409
  pairs = mcp_oauth_preserved_query_pairs(existing)
409
410
  pairs << ["code", code]
410
411
  pairs << ["state", params[:state].to_s] if params[:state].present?
412
+ pairs << ["iss", mcp_oauth_issuer]
411
413
  "#{base}?#{URI.encode_www_form(pairs)}"
412
414
  end
413
415
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module McpToolkit
4
- VERSION = "0.6.1"
4
+ VERSION = "0.6.2"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp_toolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Galanciak
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: zeitwerk
@@ -170,6 +171,7 @@ metadata:
170
171
  source_code_uri: https://github.com/BookingSync/mcp_toolkit
171
172
  changelog_uri: https://github.com/BookingSync/mcp_toolkit/blob/master/CHANGELOG.md
172
173
  rubygems_mfa_required: 'true'
174
+ post_install_message:
173
175
  rdoc_options: []
174
176
  require_paths:
175
177
  - lib
@@ -184,7 +186,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
186
  - !ruby/object:Gem::Version
185
187
  version: '0'
186
188
  requirements: []
187
- rubygems_version: 3.6.9
189
+ rubygems_version: 3.5.22
190
+ signing_key:
188
191
  specification_version: 4
189
192
  summary: Opinionated toolkit for building account-scoped, read-only MCP servers.
190
193
  test_files: []