toolchest 0.3.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/LLMS.txt +484 -0
  4. data/README.md +572 -0
  5. data/app/controllers/toolchest/oauth/authorizations_controller.rb +152 -0
  6. data/app/controllers/toolchest/oauth/authorized_applications_controller.rb +68 -0
  7. data/app/controllers/toolchest/oauth/metadata_controller.rb +68 -0
  8. data/app/controllers/toolchest/oauth/registrations_controller.rb +53 -0
  9. data/app/controllers/toolchest/oauth/tokens_controller.rb +98 -0
  10. data/app/models/toolchest/oauth_access_grant.rb +66 -0
  11. data/app/models/toolchest/oauth_access_token.rb +71 -0
  12. data/app/models/toolchest/oauth_application.rb +26 -0
  13. data/app/models/toolchest/token.rb +51 -0
  14. data/app/views/toolchest/oauth/authorizations/new.html.erb +45 -0
  15. data/app/views/toolchest/oauth/authorized_applications/index.html.erb +34 -0
  16. data/config/routes.rb +18 -0
  17. data/lib/generators/toolchest/auth_generator.rb +55 -0
  18. data/lib/generators/toolchest/consent_generator.rb +34 -0
  19. data/lib/generators/toolchest/install_generator.rb +70 -0
  20. data/lib/generators/toolchest/oauth_views_generator.rb +51 -0
  21. data/lib/generators/toolchest/skills_generator.rb +356 -0
  22. data/lib/generators/toolchest/templates/application_toolbox.rb.tt +10 -0
  23. data/lib/generators/toolchest/templates/create_toolchest_oauth.rb.tt +39 -0
  24. data/lib/generators/toolchest/templates/create_toolchest_tokens.rb.tt +16 -0
  25. data/lib/generators/toolchest/templates/initializer.rb.tt +41 -0
  26. data/lib/generators/toolchest/templates/oauth_authorize.html.erb.tt +48 -0
  27. data/lib/generators/toolchest/templates/toolbox.rb.tt +19 -0
  28. data/lib/generators/toolchest/templates/toolbox_spec.rb.tt +23 -0
  29. data/lib/generators/toolchest/toolbox_generator.rb +44 -0
  30. data/lib/toolchest/app.rb +47 -0
  31. data/lib/toolchest/auth/base.rb +15 -0
  32. data/lib/toolchest/auth/none.rb +7 -0
  33. data/lib/toolchest/auth/oauth.rb +28 -0
  34. data/lib/toolchest/auth/token.rb +73 -0
  35. data/lib/toolchest/auth_context.rb +13 -0
  36. data/lib/toolchest/configuration.rb +82 -0
  37. data/lib/toolchest/current.rb +7 -0
  38. data/lib/toolchest/endpoint.rb +13 -0
  39. data/lib/toolchest/engine.rb +95 -0
  40. data/lib/toolchest/naming.rb +31 -0
  41. data/lib/toolchest/oauth/routes.rb +25 -0
  42. data/lib/toolchest/param_definition.rb +69 -0
  43. data/lib/toolchest/parameters.rb +71 -0
  44. data/lib/toolchest/rack_app.rb +114 -0
  45. data/lib/toolchest/renderer.rb +88 -0
  46. data/lib/toolchest/router.rb +277 -0
  47. data/lib/toolchest/rspec.rb +61 -0
  48. data/lib/toolchest/sampling_builder.rb +38 -0
  49. data/lib/toolchest/tasks/toolchest.rake +123 -0
  50. data/lib/toolchest/tool_builder.rb +19 -0
  51. data/lib/toolchest/tool_definition.rb +58 -0
  52. data/lib/toolchest/toolbox.rb +312 -0
  53. data/lib/toolchest/version.rb +3 -0
  54. data/lib/toolchest.rb +89 -0
  55. metadata +122 -0
@@ -0,0 +1,45 @@
1
+ <div class="toolchest-consent">
2
+ <h1><%= @client_name %> wants access to your account</h1>
3
+ <p class="toolchest-redirect-uri">Make sure you trust this URL: <code><%= @redirect_uri %></code></p>
4
+
5
+ <%= form_tag @authorize_url, method: :post do %>
6
+ <% @oauth_params&.each do |key, value| %>
7
+ <%= hidden_field_tag key, value %>
8
+ <% end %>
9
+ <%= hidden_field_tag "original_scope", @original_scope %>
10
+
11
+ <% if @scope_list&.any? %>
12
+ <p>It's asking for:</p>
13
+ <ul class="toolchest-scope-list">
14
+ <% @scope_list.each do |s| %>
15
+ <li>
16
+ <% if @optional %>
17
+ <label>
18
+ <%= check_box_tag "scope[]", s[:name], true,
19
+ disabled: s[:required], id: "scope_#{s[:name].parameterize}" %>
20
+ <% if s[:required] %>
21
+ <%= hidden_field_tag "scope[]", s[:name] %>
22
+ <% end %>
23
+ <strong><%= s[:name] %></strong> &mdash; <%= s[:description] %>
24
+ </label>
25
+ <% else %>
26
+ <%= hidden_field_tag "scope[]", s[:name] %>
27
+ <strong><%= s[:name] %></strong> &mdash; <%= s[:description] %>
28
+ <% end %>
29
+ </li>
30
+ <% end %>
31
+ </ul>
32
+ <% end %>
33
+
34
+ <div class="toolchest-consent-actions" style="display: flex; gap: .5rem">
35
+ <%= submit_tag "Authorize", class: "toolchest-authorize-btn" %>
36
+ </div>
37
+ <% end %>
38
+
39
+ <%= form_tag @authorize_url, method: :delete do %>
40
+ <% @oauth_params&.each do |key, value| %>
41
+ <%= hidden_field_tag key, value %>
42
+ <% end %>
43
+ <%= submit_tag "Deny", class: "toolchest-deny-btn" %>
44
+ <% end %>
45
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="toolchest-authorized-apps">
2
+ <h1>Connected MCP Applications</h1>
3
+ <p>These applications have access to your account via MCP.</p>
4
+
5
+ <% if @applications.empty? %>
6
+ <p><em>No applications connected.</em></p>
7
+ <% else %>
8
+ <% @applications.each do |entry| %>
9
+ <div class="toolchest-app-entry">
10
+ <div>
11
+ <strong><%= entry[:application].name %></strong>
12
+ <div class="toolchest-app-scopes">
13
+ <% entry[:scopes].each do |scope| %>
14
+ <code><%= scope %></code>
15
+ <% end %>
16
+ </div>
17
+ <div class="toolchest-app-meta">
18
+ Connected <%= time_ago_in_words(entry[:connected_at]) %> ago
19
+ <% if entry[:last_used_at] %>
20
+ &middot; Last used <%= time_ago_in_words(entry[:last_used_at]) %> ago
21
+ <% end %>
22
+ </div>
23
+ </div>
24
+ <div>
25
+ <%= button_to "Revoke",
26
+ "#{request.script_name}/oauth/authorized_applications/#{entry[:application].id}",
27
+ method: :delete,
28
+ class: "toolchest-revoke-btn",
29
+ data: { turbo_confirm: "Revoke access for #{entry[:application].name}?" } %>
30
+ </div>
31
+ </div>
32
+ <% end %>
33
+ <% end %>
34
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,18 @@
1
+ Toolchest::Engine.routes.draw do
2
+ # OAuth endpoints (within engine mount)
3
+ get "oauth/authorize", to: "oauth/authorizations#new"
4
+ post "oauth/authorize", to: "oauth/authorizations#create"
5
+ delete "oauth/authorize", to: "oauth/authorizations#deny"
6
+ post "oauth/token", to: "oauth/tokens#create"
7
+ post "oauth/register", to: "oauth/registrations#create"
8
+
9
+ # User-facing management UI
10
+ resources :oauth_authorized_applications, only: [:index, :destroy],
11
+ path: "oauth/authorized_applications",
12
+ controller: "oauth/authorized_applications"
13
+
14
+ # MCP protocol endpoint (catch-all)
15
+ endpoint = Toolchest::Endpoint.new
16
+ match "/", to: endpoint, via: [:get, :post, :delete]
17
+ match "/*path", to: endpoint, via: [:get, :post, :delete]
18
+ end
@@ -0,0 +1,55 @@
1
+ require "rails/generators"
2
+ require "rails/generators/base"
3
+ require "rails/generators/migration"
4
+
5
+ module Toolchest
6
+ module Generators
7
+ class AuthGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def self.next_migration_number(dirname) = Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+
14
+ argument :strategy, type: :string, desc: "Auth strategy to add (token, oauth)"
15
+
16
+ def validate_strategy
17
+ unless %w[token oauth].include?(strategy)
18
+ raise Thor::Error, "Unknown auth strategy: #{strategy}. Use 'token' or 'oauth'."
19
+ end
20
+ end
21
+
22
+ def create_migration
23
+ case strategy
24
+ when "token"
25
+ migration_template "create_toolchest_tokens.rb.tt",
26
+ "db/migrate/create_toolchest_tokens.rb"
27
+ when "oauth"
28
+ migration_template "create_toolchest_oauth.rb.tt",
29
+ "db/migrate/create_toolchest_oauth.rb"
30
+ end
31
+ end
32
+
33
+ def create_consent_view
34
+ return unless strategy == "oauth"
35
+ template "oauth_authorize.html.erb.tt",
36
+ "app/views/toolchest/oauth/authorizations/new.html.erb"
37
+ end
38
+
39
+ def update_initializer
40
+ say ""
41
+ say "Auth migration created for :#{strategy}.", :green
42
+ say ""
43
+ say "Update your initializer:"
44
+ say " config.auth = :#{strategy}"
45
+ say ""
46
+ say "Then run: rails db:migrate"
47
+ say ""
48
+ end
49
+
50
+ private
51
+
52
+ def migration_version = "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,34 @@
1
+ require "rails/generators"
2
+ require "rails/generators/base"
3
+
4
+ module Toolchest
5
+ module Generators
6
+ class ConsentGenerator < Rails::Generators::Base
7
+ desc "Eject the OAuth consent view for customization"
8
+
9
+ def copy_consent_view
10
+ engine_view = File.expand_path("../../../../app/views/toolchest/oauth/authorizations/new.html.erb", __dir__)
11
+ copy_file engine_view, "app/views/toolchest/oauth/authorizations/new.html.erb"
12
+ end
13
+
14
+ def show_instructions
15
+ say ""
16
+ say "Consent view ejected!", :green
17
+ say ""
18
+ say " Customize at: app/views/toolchest/oauth/authorizations/new.html.erb"
19
+ say " It renders inside your app's layout (via yield)."
20
+ say ""
21
+ say " Available instance variables:"
22
+ say " @client_name — OAuth application name"
23
+ say " @scope_list — [{ name: 'posts:read', description: 'View posts' }, ...]"
24
+ say " @oauth_params — hash of hidden fields for the form"
25
+ say " @scope_values — raw scope strings"
26
+ say " @authorize_url — POST target for the form"
27
+ say ""
28
+ say " For full control, also eject the controller:"
29
+ say " rails g toolchest:oauth_views"
30
+ say ""
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,70 @@
1
+ require "rails/generators"
2
+ require "rails/generators/base"
3
+ require "rails/generators/migration"
4
+
5
+ module Toolchest
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ include Rails::Generators::Migration
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ def self.next_migration_number(dirname) = Time.now.utc.strftime("%Y%m%d%H%M%S")
13
+
14
+ class_option :auth, type: :string, default: "none",
15
+ desc: "Auth strategy (none, token, oauth)"
16
+
17
+ def create_application_toolbox = template "application_toolbox.rb.tt", "app/toolboxes/application_toolbox.rb"
18
+
19
+ def create_initializer = template "initializer.rb.tt", "config/initializers/toolchest.rb"
20
+
21
+ def create_toolboxes_directory = empty_directory "app/views/toolboxes"
22
+
23
+ def mount_engine
24
+ route 'mount Toolchest::Engine => "/mcp"'
25
+ route "toolchest_oauth" if auth_strategy == :oauth
26
+ end
27
+
28
+ def create_migrations
29
+ case auth_strategy
30
+ when :token
31
+ migration_template "create_toolchest_tokens.rb.tt",
32
+ "db/migrate/create_toolchest_tokens.rb"
33
+ when :oauth
34
+ migration_template "create_toolchest_oauth.rb.tt",
35
+ "db/migrate/create_toolchest_oauth.rb"
36
+ end
37
+ end
38
+
39
+ # Consent view lives in the engine and works out of the box.
40
+ # Run `rails g toolchest:consent` to eject and customize.
41
+
42
+ def show_instructions
43
+ say ""
44
+ say "Toolchest installed!", :green
45
+ say ""
46
+ say " Auth strategy: #{auth_strategy}"
47
+ say " Mount point: /mcp"
48
+ say ""
49
+ say "Next steps:"
50
+ if auth_strategy != :none
51
+ say " 1. rails db:migrate"
52
+ say " 2. rails g toolchest YourModel show create update"
53
+ say " 3. rails s → point your MCP client at http://localhost:3000/mcp"
54
+ else
55
+ say " 1. rails g toolchest YourModel show create update"
56
+ say " 2. rails s → point your MCP client at http://localhost:3000/mcp"
57
+ end
58
+ say ""
59
+ say "To change auth later: rails g toolchest:auth token (or oauth)"
60
+ say ""
61
+ end
62
+
63
+ private
64
+
65
+ def auth_strategy = options[:auth].to_sym
66
+
67
+ def migration_version = "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,51 @@
1
+ require "rails/generators"
2
+ require "rails/generators/base"
3
+
4
+ module Toolchest
5
+ module Generators
6
+ class OauthViewsGenerator < Rails::Generators::Base
7
+ desc "Eject all OAuth views and controllers for customization"
8
+
9
+ def copy_views
10
+ engine_views = File.expand_path("../../../../app/views/toolchest/oauth", __dir__)
11
+
12
+ Dir[File.join(engine_views, "**", "*.erb")].each do |src|
13
+ relative = src.sub(engine_views + "/", "")
14
+ copy_file src, "app/views/toolchest/oauth/#{relative}"
15
+ end
16
+ end
17
+
18
+ def copy_controllers
19
+ engine_controllers = File.expand_path("../../../../app/controllers/toolchest/oauth", __dir__)
20
+
21
+ Dir[File.join(engine_controllers, "**", "*.rb")].each do |src|
22
+ relative = src.sub(engine_controllers + "/", "")
23
+ copy_file src, "app/controllers/toolchest/oauth/#{relative}"
24
+ end
25
+ end
26
+
27
+ def show_instructions
28
+ say ""
29
+ say "OAuth views and controllers ejected!", :green
30
+ say ""
31
+ say " Views: app/views/toolchest/oauth/"
32
+ say " Controllers: app/controllers/toolchest/oauth/"
33
+ say ""
34
+ say "Controllers:"
35
+ say " authorizations_controller.rb — consent screen (GET/POST /mcp/oauth/authorize)"
36
+ say " tokens_controller.rb — token exchange (POST /mcp/oauth/token)"
37
+ say " registrations_controller.rb — DCR (POST /register)"
38
+ say " metadata_controller.rb — .well-known endpoints"
39
+ say " authorized_applications_controller.rb — revocation UI (GET/DELETE /mcp/oauth/authorized_applications)"
40
+ say ""
41
+ say "Views:"
42
+ say " authorizations/new.html.erb — consent page"
43
+ say " authorized_applications/index.html.erb — connected apps list"
44
+ say ""
45
+ say "All controllers inherit from ApplicationController."
46
+ say "Override any file — your app's version takes precedence."
47
+ say ""
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,356 @@
1
+ require "rails/generators"
2
+ require "rails/generators/base"
3
+
4
+ module Toolchest
5
+ module Generators
6
+ class SkillsGenerator < Rails::Generators::Base
7
+ def create_skills
8
+ create_file ".claude/skills/toolchest-add-toolbox.md", add_toolbox_skill
9
+ create_file ".claude/skills/toolchest-add-tool.md", add_tool_skill
10
+ create_file ".claude/skills/toolchest-auth.md", auth_skill
11
+
12
+ say ""
13
+ say "Claude Code skills installed!", :green
14
+ say ""
15
+ say " /add-toolbox — generate and fill in a new toolbox"
16
+ say " /add-tool — add a tool to an existing toolbox"
17
+ say " /toolchest-auth — set up or change auth"
18
+ say ""
19
+ end
20
+
21
+ private
22
+
23
+ def add_toolbox_skill
24
+ <<~'SKILL'
25
+ ---
26
+ description: Add a new toolbox to this app. Give it a model name and actions, or describe what you want.
27
+ ---
28
+
29
+ The user wants to add a new toolbox. A toolbox is a controller for MCP tools — it lives in `app/toolboxes/`.
30
+
31
+ ## Steps
32
+
33
+ 1. **Determine the model and actions.** If the user says "Orders" or "add tools for orders", look at the `Order` model (columns, associations, validations, scopes) to decide which actions make sense.
34
+
35
+ 2. **Run the generator:** `rails g toolchest:toolbox <Name> <actions...>`
36
+
37
+ 3. **Fill in the toolbox** with real tool descriptions, params, and implementations. Don't leave TODOs.
38
+
39
+ 4. **Create a partial** for the primary model: `app/views/toolboxes/<name>/_<singular>.json.jb`. This is the canonical representation — every tool that returns this model should render this partial.
40
+
41
+ 5. **Fill in views** to use the partial. `show.json.jb` should just render the partial. `index.json.jb` should map a collection through the partial.
42
+
43
+ 6. **Fill in the spec** at `spec/toolboxes/<name>_toolbox_spec.rb` with real test cases.
44
+
45
+ ## Toolbox patterns
46
+
47
+ ```ruby
48
+ class OrdersToolbox < ApplicationToolbox
49
+ # default_param adds a param to every tool (except those listed)
50
+ default_param :order_id, :string, "The order ID", except: [:create, :search]
51
+ before_action :set_order, except: [:create, :search]
52
+
53
+ rescue_from ActiveRecord::RecordNotFound do |e|
54
+ render_error "Couldn't find that #{e.model.downcase}"
55
+ end
56
+
57
+ tool "Look up an order", access: :read do
58
+ end
59
+ def show; end # implicit render of show.json.jb
60
+
61
+ tool "Search orders", access: :read do
62
+ param :status, :string, "Filter by status", optional: true, enum: %w[pending confirmed shipped]
63
+ param :customer_id, :string, "Filter by customer", optional: true
64
+ end
65
+ def search
66
+ @orders = Order.all
67
+ @orders = @orders.where(status: params[:status]) if params[:status]
68
+ @orders = @orders.where(customer_id: params[:customer_id]) if params[:customer_id]
69
+ render :index
70
+ end
71
+
72
+ tool "Create an order", access: :write do
73
+ param :customer_id, :string, "Customer"
74
+ end
75
+ def create
76
+ @order = Order.create!(params.permit(:customer_id).to_h)
77
+ render :show
78
+ suggests :show, "View the created order"
79
+ end
80
+
81
+ tool "Update an order", access: :write do
82
+ param :status, :string, "New status", enum: %w[pending confirmed shipped]
83
+ end
84
+ def update
85
+ if @order.update(params.permit(:status).to_h)
86
+ render :show
87
+ else
88
+ render_errors @order
89
+ end
90
+ end
91
+
92
+ private
93
+ def set_order = @order = Order.find(params[:order_id])
94
+ end
95
+ ```
96
+
97
+ ## View patterns — use partials
98
+
99
+ The partial is the single source of truth for how a record is rendered. Don't build bespoke views that manually list fields — use the partial.
100
+
101
+ Check the Gemfile to see whether the project uses **jb** or **jbuilder**, then use the matching syntax:
102
+
103
+ ### jb views (`.json.jb`)
104
+
105
+ ```ruby
106
+ # app/views/toolboxes/orders/_order.json.jb
107
+ {
108
+ id: order.id,
109
+ status: order.status,
110
+ customer: order.customer.name,
111
+ total: order.total.to_f,
112
+ created_at: order.created_at.iso8601
113
+ }
114
+ ```
115
+
116
+ ```ruby
117
+ # app/views/toolboxes/orders/show.json.jb
118
+ render partial: "orders/order", locals: { order: @order }
119
+ ```
120
+
121
+ ```ruby
122
+ # app/views/toolboxes/orders/index.json.jb
123
+ @orders.map { |order| render partial: "orders/order", locals: { order: order } }
124
+ ```
125
+
126
+ ### jbuilder views (`.json.jbuilder`)
127
+
128
+ ```ruby
129
+ # app/views/toolboxes/orders/_order.json.jbuilder
130
+ json.id order.id
131
+ json.status order.status
132
+ json.customer order.customer.name
133
+ json.total order.total.to_f
134
+ json.created_at order.created_at.iso8601
135
+ ```
136
+
137
+ ```ruby
138
+ # app/views/toolboxes/orders/show.json.jbuilder
139
+ json.partial! "orders/order", order: @order
140
+ ```
141
+
142
+ ```ruby
143
+ # app/views/toolboxes/orders/index.json.jbuilder
144
+ json.array! @orders, partial: "orders/order", as: :order
145
+ ```
146
+
147
+ ## Tool DSL quick reference
148
+
149
+ ```ruby
150
+ tool "Description", access: :read do # or :write
151
+ param :name, :string, "description"
152
+ param :name, :string, "description", optional: true
153
+ param :name, :string, "description", enum: %w[a b c]
154
+ param :name, :integer, "description", default: 1
155
+ param :items, [:object], "array of objects" do
156
+ param :field, :string, "nested"
157
+ end
158
+ end
159
+ ```
160
+
161
+ Types: `:string`, `:integer`, `:number`, `:boolean`, `:object`, `[:object]`, `[:string]`
162
+
163
+ ### access and annotations
164
+
165
+ Always set `access:` on tools — it controls both scope filtering and client hints:
166
+ - `access: :read` → `readOnlyHint: true, destructiveHint: false`
167
+ - `access: :write` → `readOnlyHint: false, destructiveHint: true`
168
+
169
+ Override with `annotations:` for edge cases:
170
+ ```ruby
171
+ tool "Export data", access: :read, annotations: { openWorldHint: true } do
172
+ end
173
+ ```
174
+
175
+ ### default_param
176
+
177
+ Adds a param to every tool in the toolbox. Use for the primary record ID:
178
+ ```ruby
179
+ default_param :order_id, :string, "The order ID", except: [:create, :search]
180
+ ```
181
+ `except:` and `only:` control which tools get it.
182
+
183
+ ### progress for long-running tools
184
+
185
+ ```ruby
186
+ def import
187
+ items.each_with_index do |item, i|
188
+ process(item)
189
+ mcp_progress i + 1, total: items.size, message: "Importing #{item.name}"
190
+ end
191
+ render text: "Done"
192
+ end
193
+ ```
194
+
195
+ ### sampling (ask the client's LLM)
196
+
197
+ ```ruby
198
+ def summarize
199
+ summary = mcp_sample("Summarize this order", context: @order.to_json)
200
+ render text: summary
201
+ end
202
+ ```
203
+
204
+ Block form: `mcp_sample { |s| s.system "..."; s.user "..."; s.max_tokens 500 }`
205
+
206
+ Raises `Toolchest::Error` if client doesn't support sampling — handle with `rescue_from`.
207
+
208
+ ## Testing
209
+
210
+ ```ruby
211
+ require "toolchest/rspec"
212
+
213
+ RSpec.describe OrdersToolbox, type: :toolbox do
214
+ it "shows an order" do
215
+ order = create(:order)
216
+ call_tool "orders_show", params: { order_id: order.id.to_s }, as: auth_context
217
+ expect(tool_response).to be_success
218
+ end
219
+ end
220
+ ```
221
+
222
+ Matchers: `be_success`, `be_error`, `include_text("str")`, `suggest("tool_name")`
223
+ SKILL
224
+ end
225
+
226
+ def add_tool_skill
227
+ <<~'SKILL'
228
+ ---
229
+ description: Add a new tool (action) to an existing toolbox.
230
+ ---
231
+
232
+ The user wants to add a tool to an existing toolbox.
233
+
234
+ ## Steps
235
+
236
+ 1. **Find the toolbox** in `app/toolboxes/`. Read it to understand the existing patterns — params, callbacks, error handling.
237
+
238
+ 2. **Add the tool** — a `tool` macro + `def` pair. Follow the conventions already in the file.
239
+
240
+ 3. **Add a view** if the tool renders (most do). If a partial already exists for the model, use it.
241
+
242
+ 4. **Add a test** in the existing spec file.
243
+
244
+ ## Quick reference
245
+
246
+ ```ruby
247
+ tool "Description for the LLM", access: :read do
248
+ param :query, :string, "Search query"
249
+ param :limit, :integer, "Max results", optional: true, default: 10
250
+ end
251
+ def search
252
+ @results = SomeModel.where("name LIKE ?", "%#{params[:query]}%").limit(params[:limit])
253
+ render :index
254
+ end
255
+ ```
256
+
257
+ Always set `access: :read` or `access: :write` on tools.
258
+
259
+ Instance methods available:
260
+
261
+ ```
262
+ auth.resource_owner # the user (from authenticate block)
263
+ auth.scopes # token scopes
264
+ params # declared params only
265
+ render :action # render a view
266
+ render json: { ... } # inline
267
+ render text: "..." # plain text
268
+ render_error "msg" # MCP error
269
+ render_errors @record # from ActiveModel errors
270
+ suggests :action, "hint"
271
+ mcp_log :info, "msg"
272
+ mcp_sample "prompt" # ask client's LLM
273
+ mcp_progress n, total: t, message: "..."
274
+ halt error: "forbidden"
275
+ ```
276
+
277
+ Options on `tool`:
278
+ - `access: :read` / `:write` — scope filtering + annotations
279
+ - `name: "custom_name"` — override generated tool name
280
+ - `annotations: { openWorldHint: true }` — override hints
281
+
282
+ For long-running tools, use `mcp_progress`. For tools that need LLM help, use `mcp_sample`.
283
+ SKILL
284
+ end
285
+
286
+ def auth_skill
287
+ <<~'SKILL'
288
+ ---
289
+ description: Set up or change toolchest auth strategy (none, token, oauth).
290
+ ---
291
+
292
+ The user wants to configure or change auth for their toolchest MCP endpoint.
293
+
294
+ ## Auth strategies
295
+
296
+ ### :none
297
+ No auth. `auth` is nil in toolboxes. Good for local dev.
298
+
299
+ ### :token
300
+ Bearer tokens. Simplest real auth.
301
+
302
+ ```ruby
303
+ # config/initializers/toolchest.rb
304
+ config.auth = :token
305
+ config.authenticate do |token|
306
+ User.find(token.owner_id) # becomes auth.resource_owner
307
+ end
308
+ ```
309
+
310
+ Dev setup (no DB): set `TOOLCHEST_TOKEN`, `TOOLCHEST_TOKEN_OWNER`, `TOOLCHEST_TOKEN_SCOPES` env vars.
311
+
312
+ Production: `rails g toolchest:auth token && rails db:migrate`, then `rails toolchest:token:generate OWNER=user:1`.
313
+
314
+ ### :oauth
315
+ Full OAuth 2.1 + PKCE + DCR. For Claude Desktop, Cursor, etc.
316
+
317
+ ```ruby
318
+ config.auth = :oauth
319
+ config.login_path = "/login"
320
+ config.current_user_for_oauth do |request|
321
+ request.env["warden"]&.user # or session lookup
322
+ end
323
+ config.authenticate do |token|
324
+ User.find(token.resource_owner_id)
325
+ end
326
+ ```
327
+
328
+ Needs: `rails g toolchest:auth oauth && rails db:migrate`
329
+
330
+ Routes: `mount Toolchest.app => "/mcp"` + `toolchest_oauth`
331
+
332
+ ### Scopes
333
+
334
+ ```ruby
335
+ config.scopes = {
336
+ "orders:read" => "View orders",
337
+ "orders:write" => "Modify orders"
338
+ }
339
+ # Optional: checkboxes on consent
340
+ config.optional_scopes = true
341
+ config.required_scopes = ["orders:read"]
342
+ ```
343
+
344
+ ## AuthContext
345
+
346
+ `auth` returns `Toolchest::AuthContext`:
347
+ - `auth.resource_owner` — whatever authenticate block returns
348
+ - `auth.scopes` — from the token, never lost
349
+ - `auth.token` — raw token record
350
+
351
+ The generator creates `def current_user = auth&.resource_owner` in ApplicationToolbox.
352
+ SKILL
353
+ end
354
+ end
355
+ end
356
+ end
@@ -0,0 +1,10 @@
1
+ class ApplicationToolbox < Toolchest::Toolbox
2
+ # Shared behavior for all toolboxes.
3
+ # Like ApplicationController, but for MCP tools.
4
+ #
5
+ # auth returns a Toolchest::AuthContext with:
6
+ # auth.resource_owner — whatever your authenticate block returns
7
+ # auth.scopes — token scopes (always preserved)
8
+ # auth.token — the raw token record
9
+ def current_user = auth&.resource_owner
10
+ end