mcp_toolkit 0.6.0 → 0.6.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: 375206ea98c72a64738894427abd86bf0e272f5fad3c359860cd684707e38da7
4
- data.tar.gz: e37f4ca9689b78c871daabc65110c7b5676760806d9eadaec45b30ea687f9b79
3
+ metadata.gz: af5e847538e7fe188e7fb0b0b509f0f430d2c3d33c84fa21350e30367a409578
4
+ data.tar.gz: dc3233698fd64947b42330fe51bd230e249f37e9044d303eaf1915ea213e1695
5
5
  SHA512:
6
- metadata.gz: 5920ce2f6e47c077d6f89a4078b74b82935a4be18f5a44cc415168b78e65f85aec6354ba7a3d81d1246f63d0c4d621503f5445b198ad42d80db2ab1ad129d750
7
- data.tar.gz: '091c72fd1efc0d5dbea0298af70355f21436e194d37534aa98629ea786442fa4477b00b656de0a03b8ac31485ea38d8ad783de1a59bb927f82df75922f028f79'
6
+ metadata.gz: a1fc3088f0a6532ed7a52e7cf13e13ce372edbdcddd61f255fd93cb28df6c0c5fe3a3c17ef98cedf8c5a3be5f8c72d58f8f9bdbc28eac1e7b5ae3308a83cd21f
7
+ data.tar.gz: fb9a05e81f3895dc89d65dc45a33deef371387d8ba14bf223454f666b8bdeb9c828876a66ba9f6c54b19019b416cd88dc6c66dd5ab562fdc577d1b5148872aae
data/CHANGELOG.md CHANGED
@@ -1,3 +1,55 @@
1
+ ## [0.6.1] - 2026-07-20
2
+
3
+ Two additive fixes for hosted MCP clients whose OAuth setup could not complete
4
+ against the 0.6.0 bridge: it now answers the metadata documents at the path-APPENDED
5
+ discovery locations as well as the RFC 8414 path-inserted ones, and its Dynamic
6
+ Client Registration response now states the registration (per RFC 7591) instead of
7
+ returning a bare `client_id`. Both are bridge-gated and change nothing for a host
8
+ with the bridge off, or for a client that already worked.
9
+
10
+ ### Fixed
11
+
12
+ - **Authorization-server discovery for clients that path-APPEND the well-known
13
+ segment.** For an MCP endpoint at `https://host/mcp`, the issuer is path-ful,
14
+ and RFC 8414 §3.1 places its metadata at the path-INSERTED
15
+ `/.well-known/oauth-authorization-server/mcp` — which the bridge already served.
16
+ But some MCP clients (observed with a hosted client in the wild) instead request
17
+ the path-APPENDED `https://host/mcp/.well-known/oauth-authorization-server`, got
18
+ a 404 with no fallback, never obtained a `registration_endpoint`, and reported a
19
+ registration failure. The bridge now answers the metadata at BOTH forms.
20
+
21
+ - **Dynamic Client Registration response now states the registration.** The stub
22
+ returned only a `client_id`. A strict client validates that the `redirect_uris`
23
+ it registered come back and abandons a registration that drops them — a plausible
24
+ cause of the same "couldn't register" failure, independent of the discovery gap
25
+ above. `register` now returns the client's `redirect_uris` and `client_name`
26
+ alongside `client_id_issued_at` (RFC 7591 §3.2.1). Echoing a `redirect_uri`
27
+ AUTHORIZES nothing: the bridge still stores no client, and `authorize`/`token`
28
+ check every one against the host allowlist independently, so a value reflected
29
+ here is not thereby permitted.
30
+
31
+ The `token_endpoint_auth_method` and the grant/response types are **substituted,
32
+ not echoed** — RFC 7591 §3.2.1 states the metadata as REGISTERED and lets a server
33
+ replace what it does not support. Reflecting the client's request would contradict
34
+ the discovery document that named this endpoint (`token_endpoint_auth_methods_supported:
35
+ ["none"]`, `grant_types_supported: ["authorization_code"]`) and promise a flow the
36
+ token endpoint rejects — a reflected `refresh_token` is a refresh answered
37
+ `unsupported_grant_type`. The supported sets are now named once and shared by both
38
+ documents, so they cannot drift apart.
39
+
40
+ ### Added
41
+
42
+ - **Path-appended metadata routes under the engine mount** (drawn only when
43
+ `oauth_bridge?`): `GET <mcp>/.well-known/oauth-authorization-server`,
44
+ `GET <mcp>/.well-known/oauth-protected-resource`, and the OIDC discovery alias
45
+ `GET <mcp>/.well-known/openid-configuration` (which returns the
46
+ authorization-server document). All stay UNDER the mount, so they claim nothing
47
+ origin-global and cannot collide with an OAuth provider the host already runs;
48
+ the RFC 8414 path-inserted documents the host draws at the origin root are
49
+ unchanged. Every identifier is still derived from the live request origin, so
50
+ the document served at an appended location is byte-identical to the inserted
51
+ one, and both carry `Cache-Control: no-store`.
52
+
1
53
  ## [0.6.0] - 2026-07-16
2
54
 
3
55
  An OAuth 2.1 authorization bridge for the authority role, so hosted MCP clients
data/README.md CHANGED
@@ -543,16 +543,26 @@ origin-global.** The flow endpoints live under the engine's mount
543
543
  `/oauth/*` — as an app with Doorkeeper for its own API does — you keep every one of
544
544
  those routes.
545
545
 
546
- The metadata documents are **path-scoped** to the mount
547
- (`/.well-known/oauth-protected-resource/mcp`), never the bare
548
- `/.well-known/oauth-authorization-server`. That matters: the bare paths are
549
- origin-global and mean *"the authorization server of this whole origin"*, which
550
- belongs to a provider you already run, not to an MCP server sharing the host.
546
+ The metadata documents are **path-scoped** to the mount, never the bare
547
+ origin-global `/.well-known/oauth-authorization-server`. That matters: the bare
548
+ paths are origin-global and mean *"the authorization server of this whole origin"*,
549
+ which belongs to a provider you already run, not to an MCP server sharing the host.
551
550
  RFC 8414 §3.1 exists for exactly this — *"Using path components enables supporting
552
551
  multiple issuers per host"* — and the MCP authorization spec (2025-11-25) requires
553
552
  a client given a path-ful issuer to try the path-**inserted** URLs, with no root
554
553
  fallback. So the issuer is your MCP endpoint URL, and both documents hang off it.
555
554
 
555
+ A path-ful issuer, though, has two readings of *where* under the origin its
556
+ metadata lives, and MCP clients disagree: some **insert** the well-known segment
557
+ before the resource path (`/.well-known/oauth-authorization-server/mcp` — the RFC
558
+ form the host draws), others **append** it after
559
+ (`/mcp/.well-known/oauth-authorization-server`). The bridge serves both: the
560
+ inserted forms at the origin root (`draw_oauth_metadata_routes`), and the appended
561
+ forms — plus the `openid-configuration` OIDC alias — under the engine mount,
562
+ automatically whenever the bridge is on. Both stay path-scoped and claim nothing
563
+ origin-global; a client discovers the authorization server whichever convention it
564
+ follows.
565
+
556
566
  If your MCP endpoint IS its origin root (a dedicated MCP domain), there is no path
557
567
  to insert and you get the bare paths — correct there, since your server really is
558
568
  that origin's only authorization server. Set `oauth_resource_path = "/"`.
data/config/routes.rb CHANGED
@@ -38,17 +38,31 @@ McpToolkit::Engine.routes.draw do
38
38
  # route above: the routes file is evaluated through the routes_reloader, after
39
39
  # the host's initializers/to_prepare, so the config is already set.
40
40
  #
41
- # The two metadata documents are NOT here: a client looks for them at the origin
42
- # root, which an engine mounted under a path cannot draw. The host draws them
43
- # with `McpToolkit.draw_oauth_metadata_routes(self)`.
44
- # `format: false` on each, as on the metadata routes the host draws: without it
45
- # Rails' optional `(.:format)` segment matches, so `/mcp/oauth/authorize.json`
46
- # reaches the action, finds no JSON template, and 500s — an unauthenticated
47
- # error on a public endpoint, for a format the bridge never speaks.
41
+ # `format: false` on each: without it Rails' optional `(.:format)` segment
42
+ # matches, so `/mcp/oauth/authorize.json` reaches the action, finds no JSON
43
+ # template, and 500s — an unauthenticated error on a public endpoint, for a
44
+ # format the bridge never speaks.
48
45
  if McpToolkit.config.oauth_bridge?
49
46
  get "oauth/authorize", to: "oauth#authorize", format: false
50
47
  post "oauth/authorize", to: "oauth#approve", format: false
51
48
  post "oauth/token", to: "oauth#token", format: false
52
49
  post "oauth/register", to: "oauth#register", format: false
50
+
51
+ # The metadata documents, ALSO served path-APPENDED to the mount
52
+ # (`/mcp/.well-known/oauth-authorization-server`), in addition to the RFC 8414
53
+ # §3.1 / RFC 9728 §3.1 path-INSERTED locations the host draws at the origin
54
+ # root via `McpToolkit.draw_oauth_metadata_routes`. A path-ful issuer has two
55
+ # readings of where its metadata lives, and MCP clients disagree: some INSERT
56
+ # the well-known segment before the resource path (the RFC form), others APPEND
57
+ # it after. Observed in the wild — a hosted client requesting
58
+ # `<mcp>/.well-known/oauth-authorization-server` and getting a 404 it could not
59
+ # recover from, so it never reached registration. Serving both meets either
60
+ # convention. Unlike the origin-root bare paths, these stay UNDER the mount, so
61
+ # they claim nothing origin-global and cannot collide with an OAuth provider
62
+ # the host already runs. `openid-configuration` is the OIDC discovery alias a
63
+ # client may probe instead; it answers the same authorization-server document.
64
+ get ".well-known/oauth-authorization-server", to: "oauth#authorization_server", format: false
65
+ get ".well-known/oauth-protected-resource", to: "oauth#protected_resource", format: false
66
+ get ".well-known/openid-configuration", to: "oauth#authorization_server", format: false
53
67
  end
54
68
  end
@@ -37,6 +37,12 @@ module McpToolkit::Oauth::ControllerMethods
37
37
  # base64url of a SHA-256, which is always exactly 43 of the same alphabet.
38
38
  PKCE_VALUE = /\A[A-Za-z0-9\-._~]{43,128}\z/
39
39
 
40
+ # What the token endpoint actually honours. Named once because the discovery
41
+ # document and the registration response MUST agree — a client reads the first
42
+ # to find the second, so a disagreement is this server contradicting itself.
43
+ SUPPORTED_GRANT_TYPES = %w[authorization_code].freeze
44
+ SUPPORTED_RESPONSE_TYPES = %w[code].freeze
45
+
40
46
  included do
41
47
  # Safe to disable: the token endpoint is called server-to-server without a CSRF
42
48
  # token, and `approve` never acts on ambient authority — it reads no session and
@@ -86,22 +92,38 @@ module McpToolkit::Oauth::ControllerMethods
86
92
  authorization_endpoint: mcp_oauth_endpoint_url("authorize"),
87
93
  token_endpoint: mcp_oauth_endpoint_url("token"),
88
94
  registration_endpoint: mcp_oauth_endpoint_url("register"),
89
- response_types_supported: ["code"],
90
- grant_types_supported: ["authorization_code"],
95
+ response_types_supported: SUPPORTED_RESPONSE_TYPES,
96
+ grant_types_supported: SUPPORTED_GRANT_TYPES,
91
97
  code_challenge_methods_supported: ["S256"],
92
98
  token_endpoint_auth_methods_supported: ["none"]
93
99
  }
94
100
  end
95
101
 
96
- # Stateless: persisting a `client_id` nothing reads would only grow a table of
97
- # strings the bridge never consults.
102
+ # Stateless: nothing here is persisted (no endpoint reads a `client_id`). The
103
+ # response still names the `redirect_uris` the client sent, because a strict
104
+ # client validates that they come back and abandons a registration that drops
105
+ # them — the failure a hosted client hit against the pre-0.6.1 stub, which
106
+ # returned only a `client_id`. That echo AUTHORIZES nothing: `authorize` and
107
+ # `token` check every `redirect_uri` against the host's allowlist independently.
108
+ #
109
+ # The auth method and the grant/response types are SUBSTITUTED rather than
110
+ # echoed — RFC 7591 §3.2.1 states the metadata as REGISTERED, and lets a server
111
+ # replace what it does not support. Reflecting the client's request instead
112
+ # would contradict `authorization_server`, which is where that client got this
113
+ # endpoint, and would promise a flow `token` rejects: a `refresh_token` echoed
114
+ # back is a refresh answered `unsupported_grant_type`. No `client_secret` is
115
+ # issued, so `none` is the only auth method a client here could perform.
98
116
  def register
99
- render json: {
117
+ body = {
100
118
  client_id: SecureRandom.uuid,
119
+ client_id_issued_at: Time.now.to_i,
120
+ redirect_uris: mcp_oauth_param_list(:redirect_uris),
101
121
  token_endpoint_auth_method: "none",
102
- grant_types: ["authorization_code"],
103
- response_types: ["code"]
104
- }, status: :created
122
+ grant_types: mcp_oauth_registered_subset(:grant_types, SUPPORTED_GRANT_TYPES),
123
+ response_types: mcp_oauth_registered_subset(:response_types, SUPPORTED_RESPONSE_TYPES)
124
+ }
125
+ body[:client_name] = params[:client_name].to_s if params[:client_name].present?
126
+ render json: body, status: :created
105
127
  end
106
128
 
107
129
  # `formats: [:html]` because there is only an HTML template and `Accept` picks
@@ -151,6 +173,19 @@ module McpToolkit::Oauth::ControllerMethods
151
173
  McpToolkit.config
152
174
  end
153
175
 
176
+ # A registration parameter as a plain array of strings — RFC 7591 lists arrive
177
+ # as JSON arrays; empty when the client sent none.
178
+ def mcp_oauth_param_list(key)
179
+ Array(params[key]).map(&:to_s)
180
+ end
181
+
182
+ # What the client asked for, narrowed to what this server honours. A client that
183
+ # asked for NOTHING supported gets the default rather than an empty array, which
184
+ # would state a registration able to do nothing at all.
185
+ def mcp_oauth_registered_subset(key, supported)
186
+ (mcp_oauth_param_list(key) & supported).presence || supported
187
+ end
188
+
154
189
  # ---- request validation ---------------------------------------------------
155
190
 
156
191
  # Halts both legs before their action runs. Both problems RENDER rather than
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module McpToolkit
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp_toolkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Galanciak