toolchest 0.3.3 → 0.3.5
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/README.md +57 -1
- data/app/controllers/toolchest/application_controller.rb +5 -0
- data/app/controllers/toolchest/oauth/authorizations_controller.rb +1 -1
- data/app/controllers/toolchest/oauth/authorized_applications_controller.rb +1 -1
- data/app/controllers/toolchest/oauth/metadata_controller.rb +1 -1
- data/app/controllers/toolchest/oauth/registrations_controller.rb +9 -3
- data/app/controllers/toolchest/oauth/tokens_controller.rb +4 -3
- data/app/models/toolchest/oauth_access_grant.rb +7 -0
- data/lib/generators/toolchest/consent_generator.rb +3 -2
- data/lib/generators/toolchest/oauth_views_generator.rb +10 -8
- data/lib/toolchest/renderer.rb +12 -0
- data/lib/toolchest/route_delegation.rb +27 -0
- data/lib/toolchest/router.rb +6 -1
- data/lib/toolchest/toolbox.rb +39 -0
- data/lib/toolchest/version.rb +1 -1
- data/lib/toolchest.rb +18 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db7d29ec671adac8f24512bf3d190dedb55250dfd846b31c5906feac16771130
|
|
4
|
+
data.tar.gz: a3c7414f1c60e5997942fdacfa70ee3a368e6a0cd2b237eab2b1d187351cb10d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68779c02c513ba5dc8052407a015de6fddda8e44c5aa6add4daeb204eb08fe7b41b997a6a2791e90b62ab1dc558d467d3ad722cbee5baca32fccbe7a04692203
|
|
7
|
+
data.tar.gz: d63e96b09cdf89b09a31e902b593f55b4399ab347c5af3daa033c50c03054c654e0338fe2f9eb31544f19522e65bb1c30f9181c33a040e14a2c26dbf91b5371c
|
data/README.md
CHANGED
|
@@ -31,6 +31,8 @@ rails s
|
|
|
31
31
|
```ruby
|
|
32
32
|
# app/toolboxes/application_toolbox.rb
|
|
33
33
|
class ApplicationToolbox < Toolchest::Toolbox
|
|
34
|
+
helper_method :current_user
|
|
35
|
+
|
|
34
36
|
def current_user = auth&.resource_owner
|
|
35
37
|
|
|
36
38
|
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
@@ -39,7 +41,7 @@ class ApplicationToolbox < Toolchest::Toolbox
|
|
|
39
41
|
end
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
This is your ApplicationController. `auth` returns a `Toolchest::AuthContext` with `.resource_owner` (whatever your `authenticate` block returns), `.scopes` (always from the token), and `.token` (the raw record). Define `current_user` as a convenience
|
|
44
|
+
This is your ApplicationController. `auth` returns a `Toolchest::AuthContext` with `.resource_owner` (whatever your `authenticate` block returns), `.scopes` (always from the token), and `.token` (the raw record). Define `current_user` as a convenience and expose it to views with `helper_method`, add shared error handling, include your gems.
|
|
43
45
|
|
|
44
46
|
```ruby
|
|
45
47
|
# app/toolboxes/orders_toolbox.rb
|
|
@@ -130,6 +132,41 @@ render_errors @order # MCP error from ActiveModel errors
|
|
|
130
132
|
|
|
131
133
|
`render :show` after mutations. Most toolboxes need one or two views.
|
|
132
134
|
|
|
135
|
+
### View helpers
|
|
136
|
+
|
|
137
|
+
Toolbox methods aren't available in views by default — views only get instance variables. Use `helper_method` to expose methods, same as a Rails controller:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class ApplicationToolbox < Toolchest::Toolbox
|
|
141
|
+
helper_method :current_user, :admin?
|
|
142
|
+
|
|
143
|
+
def current_user = auth&.resource_owner
|
|
144
|
+
def admin? = current_user&.admin?
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Now `current_user` and `admin?` work in any view:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
# app/views/toolboxes/orders/show.json.jb
|
|
152
|
+
{
|
|
153
|
+
id: @order.id,
|
|
154
|
+
status: @order.status,
|
|
155
|
+
viewer: current_user.name,
|
|
156
|
+
can_edit: admin?
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Include entire modules with `helper`:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class ApplicationToolbox < Toolchest::Toolbox
|
|
164
|
+
helper FormattingHelper
|
|
165
|
+
end
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Both `helper_method` and `helper` inherit through the toolbox hierarchy — define them in `ApplicationToolbox` and every toolbox gets them.
|
|
169
|
+
|
|
133
170
|
### Resources and prompts
|
|
134
171
|
|
|
135
172
|
Supported, though most toolboxes won't need them.
|
|
@@ -360,6 +397,25 @@ Toolchest::OauthAccessGrant.revoke_all_for(app, user.id)
|
|
|
360
397
|
app.destroy # cascades to all grants and tokens
|
|
361
398
|
```
|
|
362
399
|
|
|
400
|
+
### Controller behavior
|
|
401
|
+
|
|
402
|
+
The consent screen and authorized applications page inherit from your app's `ApplicationController` by default — so they get your auth, layout, nav, etc. Route helpers like `root_path` in your layout work automatically (Toolchest delegates unresolved `_path`/`_url` helpers to `main_app`).
|
|
403
|
+
|
|
404
|
+
If you don't want host app behavior on these pages:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
# config/initializers/toolchest.rb
|
|
408
|
+
Toolchest.base_controller = "ActionController::Base"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
To disable the route helper delegation:
|
|
412
|
+
|
|
413
|
+
```ruby
|
|
414
|
+
Toolchest.delegate_route_helpers = false
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
API-only endpoints (token exchange, metadata, dynamic registration) always use `ActionController::API` regardless of this setting.
|
|
418
|
+
|
|
363
419
|
### Custom
|
|
364
420
|
|
|
365
421
|
If the built-in strategies don't fit, pass any object that responds to `#authenticate(request)`:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Toolchest
|
|
2
2
|
module Oauth
|
|
3
|
-
class RegistrationsController < ::
|
|
4
|
-
skip_forgery_protection
|
|
3
|
+
class RegistrationsController < ActionController::API
|
|
5
4
|
wrap_parameters false
|
|
6
5
|
|
|
7
6
|
# POST /register — Dynamic Client Registration (RFC 7591)
|
|
@@ -9,7 +8,14 @@ module Toolchest
|
|
|
9
8
|
# at authorization time via the resource param.
|
|
10
9
|
def create
|
|
11
10
|
name = (params[:client_name] || "MCP Client").truncate(255)
|
|
12
|
-
uris = Array(params[:redirect_uris])
|
|
11
|
+
uris = Array(params[:redirect_uris]).map(&:to_s)
|
|
12
|
+
|
|
13
|
+
if uris.any? { |u| u.match?(/[\r\n]/) }
|
|
14
|
+
return render json: {
|
|
15
|
+
error: "invalid_client_metadata",
|
|
16
|
+
error_description: "Redirect URIs must not contain newlines"
|
|
17
|
+
}, status: :bad_request
|
|
18
|
+
end
|
|
13
19
|
|
|
14
20
|
if uris.size > 10
|
|
15
21
|
return render json: {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
module Toolchest
|
|
2
2
|
module Oauth
|
|
3
|
-
class TokensController < ::
|
|
4
|
-
skip_forgery_protection
|
|
3
|
+
class TokensController < ActionController::API
|
|
5
4
|
|
|
6
5
|
# POST /mcp/oauth/token
|
|
7
6
|
def create
|
|
@@ -46,7 +45,9 @@ module Toolchest
|
|
|
46
45
|
return error_response("invalid_grant", "PKCE verification failed")
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
grant.
|
|
48
|
+
unless grant.revoke_atomically!
|
|
49
|
+
return error_response("invalid_grant", "Authorization code already used")
|
|
50
|
+
end
|
|
50
51
|
|
|
51
52
|
token = Toolchest::OauthAccessToken.create_for(
|
|
52
53
|
application: app,
|
|
@@ -24,6 +24,13 @@ module Toolchest
|
|
|
24
24
|
|
|
25
25
|
def revoke! = update!(revoked_at: Time.current)
|
|
26
26
|
|
|
27
|
+
# Atomic revocation — returns true only if THIS call revoked the grant.
|
|
28
|
+
# Prevents race conditions where two concurrent token exchanges both
|
|
29
|
+
# find the grant active and both mint tokens (RFC 6749 §4.1.2).
|
|
30
|
+
def revoke_atomically!
|
|
31
|
+
self.class.where(id: id, revoked_at: nil).update_all(revoked_at: Time.current) > 0
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def uses_pkce? = code_challenge.present?
|
|
28
35
|
|
|
29
36
|
def verify_pkce(code_verifier)
|
|
@@ -4,11 +4,12 @@ require "rails/generators/base"
|
|
|
4
4
|
module Toolchest
|
|
5
5
|
module Generators
|
|
6
6
|
class ConsentGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("../../../..", __dir__)
|
|
8
|
+
|
|
7
9
|
desc "Eject the OAuth consent view for customization"
|
|
8
10
|
|
|
9
11
|
def copy_consent_view
|
|
10
|
-
|
|
11
|
-
copy_file engine_view, "app/views/toolchest/oauth/authorizations/new.html.erb"
|
|
12
|
+
copy_file "app/views/toolchest/oauth/authorizations/new.html.erb"
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
def show_instructions
|
|
@@ -4,23 +4,25 @@ require "rails/generators/base"
|
|
|
4
4
|
module Toolchest
|
|
5
5
|
module Generators
|
|
6
6
|
class OauthViewsGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("../../../..", __dir__)
|
|
8
|
+
|
|
7
9
|
desc "Eject all OAuth views and controllers for customization"
|
|
8
10
|
|
|
9
11
|
def copy_views
|
|
10
|
-
|
|
12
|
+
views_dir = File.join(self.class.source_root, "app/views/toolchest/oauth")
|
|
11
13
|
|
|
12
|
-
Dir[File.join(
|
|
13
|
-
relative = src.sub(
|
|
14
|
-
copy_file
|
|
14
|
+
Dir[File.join(views_dir, "**", "*.erb")].each do |src|
|
|
15
|
+
relative = src.sub(views_dir + "/", "")
|
|
16
|
+
copy_file "app/views/toolchest/oauth/#{relative}"
|
|
15
17
|
end
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def copy_controllers
|
|
19
|
-
|
|
21
|
+
controllers_dir = File.join(self.class.source_root, "app/controllers/toolchest/oauth")
|
|
20
22
|
|
|
21
|
-
Dir[File.join(
|
|
22
|
-
relative = src.sub(
|
|
23
|
-
copy_file
|
|
23
|
+
Dir[File.join(controllers_dir, "**", "*.rb")].each do |src|
|
|
24
|
+
relative = src.sub(controllers_dir + "/", "")
|
|
25
|
+
copy_file "app/controllers/toolchest/oauth/#{relative}"
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
|
data/lib/toolchest/renderer.rb
CHANGED
|
@@ -12,6 +12,7 @@ module Toolchest
|
|
|
12
12
|
|
|
13
13
|
lookup = ActionView::LookupContext.new(view_paths)
|
|
14
14
|
view = ActionView::Base.with_empty_template_cache.new(lookup, assigns, nil)
|
|
15
|
+
apply_helpers(view, toolbox)
|
|
15
16
|
|
|
16
17
|
result = view.render(template: template_name, formats: [:json])
|
|
17
18
|
|
|
@@ -73,6 +74,17 @@ module Toolchest
|
|
|
73
74
|
assigns
|
|
74
75
|
end
|
|
75
76
|
|
|
77
|
+
def apply_helpers(view, toolbox)
|
|
78
|
+
toolbox.class.helper_modules.each { |mod| view.extend(mod) }
|
|
79
|
+
|
|
80
|
+
toolbox.class.helper_methods.each do |method_name|
|
|
81
|
+
tb = toolbox
|
|
82
|
+
view.define_singleton_method(method_name) do |*args, **kwargs, &block|
|
|
83
|
+
tb.send(method_name, *args, **kwargs, &block)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
76
88
|
def view_paths
|
|
77
89
|
paths = []
|
|
78
90
|
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Toolchest
|
|
2
|
+
# Delegates unresolved _path/_url helpers to main_app so the host
|
|
3
|
+
# application's layout works inside engine-rendered views without
|
|
4
|
+
# requiring main_app. prefixes everywhere.
|
|
5
|
+
#
|
|
6
|
+
# Included as a view helper by the engine when
|
|
7
|
+
# Toolchest.delegate_route_helpers is true (the default).
|
|
8
|
+
#
|
|
9
|
+
# To disable:
|
|
10
|
+
#
|
|
11
|
+
# # config/initializers/toolchest.rb
|
|
12
|
+
# Toolchest.delegate_route_helpers = false
|
|
13
|
+
#
|
|
14
|
+
module RouteDelegation
|
|
15
|
+
def method_missing(method, *args, **kwargs, &block)
|
|
16
|
+
if method.to_s.end_with?("_path", "_url") && main_app.respond_to?(method)
|
|
17
|
+
main_app.public_send(method, *args, **kwargs, &block)
|
|
18
|
+
else
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def respond_to_missing?(method, include_private = false)
|
|
24
|
+
(method.to_s.end_with?("_path", "_url") && main_app.respond_to?(method)) || super
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/toolchest/router.rb
CHANGED
|
@@ -217,7 +217,12 @@ module Toolchest
|
|
|
217
217
|
READ_ACTIONS = Set.new(%i[show index list search]).freeze
|
|
218
218
|
|
|
219
219
|
def tool_allowed_by_scopes?(tool_definition, scopes)
|
|
220
|
-
|
|
220
|
+
if scopes.empty?
|
|
221
|
+
# Token has no scopes. Allow only if the server hasn't declared any
|
|
222
|
+
# scopes either (convention-based filtering without declarations).
|
|
223
|
+
# When scopes ARE configured, empty token scopes = no access (fail closed).
|
|
224
|
+
return Toolchest.configuration(@mount_key).scopes.empty?
|
|
225
|
+
end
|
|
221
226
|
|
|
222
227
|
if tool_definition.scope
|
|
223
228
|
return tool_definition.scope.any? { |s| scopes.include?(s) }
|
data/lib/toolchest/toolbox.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Toolchest
|
|
|
16
16
|
subclass.instance_variable_set(:@_resources, [])
|
|
17
17
|
subclass.instance_variable_set(:@_prompts, [])
|
|
18
18
|
subclass.instance_variable_set(:@_pending_tool, nil)
|
|
19
|
+
subclass.instance_variable_set(:@_helper_methods, [])
|
|
20
|
+
subclass.instance_variable_set(:@_helper_modules, [])
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def tool_definitions
|
|
@@ -117,6 +119,39 @@ module Toolchest
|
|
|
117
119
|
@_tool_definitions[method_name.to_sym] = definition
|
|
118
120
|
end
|
|
119
121
|
|
|
122
|
+
# Expose toolbox methods as view helpers, like controller helper_method.
|
|
123
|
+
#
|
|
124
|
+
# helper_method :current_user, :admin?
|
|
125
|
+
#
|
|
126
|
+
def helper_method(*methods)
|
|
127
|
+
@_helper_methods.concat(methods.map(&:to_sym))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Include modules as view helpers.
|
|
131
|
+
#
|
|
132
|
+
# helper ApplicationHelper
|
|
133
|
+
# helper FormattingHelper, CurrencyHelper
|
|
134
|
+
#
|
|
135
|
+
def helper(*modules)
|
|
136
|
+
@_helper_modules.concat(modules)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def helper_methods
|
|
140
|
+
ancestors
|
|
141
|
+
.select { |a| a.respond_to?(:own_helper_methods, true) }
|
|
142
|
+
.reverse
|
|
143
|
+
.flat_map { |a| a.send(:own_helper_methods) }
|
|
144
|
+
.uniq
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def helper_modules
|
|
148
|
+
ancestors
|
|
149
|
+
.select { |a| a.respond_to?(:own_helper_modules, true) }
|
|
150
|
+
.reverse
|
|
151
|
+
.flat_map { |a| a.send(:own_helper_modules) }
|
|
152
|
+
.uniq
|
|
153
|
+
end
|
|
154
|
+
|
|
120
155
|
def controller_name = name&.underscore&.chomp("_toolbox") || "anonymous"
|
|
121
156
|
|
|
122
157
|
protected
|
|
@@ -128,6 +163,10 @@ module Toolchest
|
|
|
128
163
|
def own_resources = @_resources || []
|
|
129
164
|
|
|
130
165
|
def own_prompts = @_prompts || []
|
|
166
|
+
|
|
167
|
+
def own_helper_methods = @_helper_methods || []
|
|
168
|
+
|
|
169
|
+
def own_helper_modules = @_helper_modules || []
|
|
131
170
|
end
|
|
132
171
|
|
|
133
172
|
attr_reader :params
|
data/lib/toolchest/version.rb
CHANGED
data/lib/toolchest.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "toolchest/version"
|
|
|
4
4
|
|
|
5
5
|
module Toolchest
|
|
6
6
|
autoload :App, "toolchest/app"
|
|
7
|
+
autoload :RouteDelegation, "toolchest/route_delegation"
|
|
7
8
|
autoload :AuthContext, "toolchest/auth_context"
|
|
8
9
|
autoload :Configuration, "toolchest/configuration"
|
|
9
10
|
autoload :Current, "toolchest/current"
|
|
@@ -68,11 +69,28 @@ module Toolchest
|
|
|
68
69
|
# When multiple OAuth mounts exist, bare /.well-known/* resolves to this mount
|
|
69
70
|
attr_accessor :default_oauth_mount
|
|
70
71
|
|
|
72
|
+
# Parent class for engine HTML controllers. Default: "::ApplicationController".
|
|
73
|
+
# Set to "ActionController::Base" to avoid inheriting host app behavior.
|
|
74
|
+
attr_writer :base_controller
|
|
75
|
+
def base_controller
|
|
76
|
+
@base_controller || "::ApplicationController"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Delegate unresolved _path/_url helpers to main_app so the host
|
|
80
|
+
# layout renders correctly inside engine views. Default: true.
|
|
81
|
+
# Set to false in an initializer to disable.
|
|
82
|
+
attr_writer :delegate_route_helpers
|
|
83
|
+
def delegate_route_helpers
|
|
84
|
+
@delegate_route_helpers != false
|
|
85
|
+
end
|
|
86
|
+
|
|
71
87
|
def reset!
|
|
72
88
|
@configs = nil
|
|
73
89
|
@routers = nil
|
|
74
90
|
@apps = nil
|
|
75
91
|
@default_oauth_mount = nil
|
|
92
|
+
@base_controller = nil
|
|
93
|
+
@delegate_route_helpers = nil
|
|
76
94
|
end
|
|
77
95
|
|
|
78
96
|
# Reset only routers/apps (preserves config set by initializers)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toolchest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nora
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- LICENSE
|
|
47
47
|
- LLMS.txt
|
|
48
48
|
- README.md
|
|
49
|
+
- app/controllers/toolchest/application_controller.rb
|
|
49
50
|
- app/controllers/toolchest/oauth/authorizations_controller.rb
|
|
50
51
|
- app/controllers/toolchest/oauth/authorized_applications_controller.rb
|
|
51
52
|
- app/controllers/toolchest/oauth/metadata_controller.rb
|
|
@@ -88,6 +89,7 @@ files:
|
|
|
88
89
|
- lib/toolchest/parameters.rb
|
|
89
90
|
- lib/toolchest/rack_app.rb
|
|
90
91
|
- lib/toolchest/renderer.rb
|
|
92
|
+
- lib/toolchest/route_delegation.rb
|
|
91
93
|
- lib/toolchest/router.rb
|
|
92
94
|
- lib/toolchest/rspec.rb
|
|
93
95
|
- lib/toolchest/sampling_builder.rb
|