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 +4 -4
- data/CHANGELOG.md +52 -0
- data/README.md +15 -5
- data/config/routes.rb +21 -7
- data/lib/mcp_toolkit/oauth/controller_methods.rb +43 -8
- data/lib/mcp_toolkit/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af5e847538e7fe188e7fb0b0b509f0f430d2c3d33c84fa21350e30367a409578
|
|
4
|
+
data.tar.gz: dc3233698fd64947b42330fe51bd230e249f37e9044d303eaf1915ea213e1695
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
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:
|
|
90
|
-
grant_types_supported:
|
|
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:
|
|
97
|
-
#
|
|
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
|
-
|
|
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:
|
|
103
|
-
response_types:
|
|
104
|
-
}
|
|
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
|
data/lib/mcp_toolkit/version.rb
CHANGED