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 +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +202 -7
- data/lib/mcp_toolkit/oauth/controller_methods.rb +4 -2
- data/lib/mcp_toolkit/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa488bbd5b90e15c5f7e9a70f8792be1ec070b0fdd6ce6e89f1279bd24bf59cf
|
|
4
|
+
data.tar.gz: 2e8953c00c9b6dab828efcd9c92f71990d8e09477f15851aa9976f0b942456ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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` |
|
|
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!`,
|
|
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
|
|
689
|
-
|
|
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
|
|
data/lib/mcp_toolkit/version.rb
CHANGED
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.
|
|
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:
|
|
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.
|
|
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: []
|