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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/LLMS.txt +484 -0
- data/README.md +572 -0
- data/app/controllers/toolchest/oauth/authorizations_controller.rb +152 -0
- data/app/controllers/toolchest/oauth/authorized_applications_controller.rb +68 -0
- data/app/controllers/toolchest/oauth/metadata_controller.rb +68 -0
- data/app/controllers/toolchest/oauth/registrations_controller.rb +53 -0
- data/app/controllers/toolchest/oauth/tokens_controller.rb +98 -0
- data/app/models/toolchest/oauth_access_grant.rb +66 -0
- data/app/models/toolchest/oauth_access_token.rb +71 -0
- data/app/models/toolchest/oauth_application.rb +26 -0
- data/app/models/toolchest/token.rb +51 -0
- data/app/views/toolchest/oauth/authorizations/new.html.erb +45 -0
- data/app/views/toolchest/oauth/authorized_applications/index.html.erb +34 -0
- data/config/routes.rb +18 -0
- data/lib/generators/toolchest/auth_generator.rb +55 -0
- data/lib/generators/toolchest/consent_generator.rb +34 -0
- data/lib/generators/toolchest/install_generator.rb +70 -0
- data/lib/generators/toolchest/oauth_views_generator.rb +51 -0
- data/lib/generators/toolchest/skills_generator.rb +356 -0
- data/lib/generators/toolchest/templates/application_toolbox.rb.tt +10 -0
- data/lib/generators/toolchest/templates/create_toolchest_oauth.rb.tt +39 -0
- data/lib/generators/toolchest/templates/create_toolchest_tokens.rb.tt +16 -0
- data/lib/generators/toolchest/templates/initializer.rb.tt +41 -0
- data/lib/generators/toolchest/templates/oauth_authorize.html.erb.tt +48 -0
- data/lib/generators/toolchest/templates/toolbox.rb.tt +19 -0
- data/lib/generators/toolchest/templates/toolbox_spec.rb.tt +23 -0
- data/lib/generators/toolchest/toolbox_generator.rb +44 -0
- data/lib/toolchest/app.rb +47 -0
- data/lib/toolchest/auth/base.rb +15 -0
- data/lib/toolchest/auth/none.rb +7 -0
- data/lib/toolchest/auth/oauth.rb +28 -0
- data/lib/toolchest/auth/token.rb +73 -0
- data/lib/toolchest/auth_context.rb +13 -0
- data/lib/toolchest/configuration.rb +82 -0
- data/lib/toolchest/current.rb +7 -0
- data/lib/toolchest/endpoint.rb +13 -0
- data/lib/toolchest/engine.rb +95 -0
- data/lib/toolchest/naming.rb +31 -0
- data/lib/toolchest/oauth/routes.rb +25 -0
- data/lib/toolchest/param_definition.rb +69 -0
- data/lib/toolchest/parameters.rb +71 -0
- data/lib/toolchest/rack_app.rb +114 -0
- data/lib/toolchest/renderer.rb +88 -0
- data/lib/toolchest/router.rb +277 -0
- data/lib/toolchest/rspec.rb +61 -0
- data/lib/toolchest/sampling_builder.rb +38 -0
- data/lib/toolchest/tasks/toolchest.rake +123 -0
- data/lib/toolchest/tool_builder.rb +19 -0
- data/lib/toolchest/tool_definition.rb +58 -0
- data/lib/toolchest/toolbox.rb +312 -0
- data/lib/toolchest/version.rb +3 -0
- data/lib/toolchest.rb +89 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9c78a7f55b37d501c0e2d5c4e164048ea4d176f2eb7126c232953a1c4c52e1da
|
|
4
|
+
data.tar.gz: 403d4c3b8ee52953630e52f0d4d31f1a31e9d406511fdfdd7c4f47a67f083bf6
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c4c8ca2520fc43e69398fffa2f25a08db7ec0566c7f9453f5fb5d442744e4c42be5ac53ffcb037ad5181a4bac8395970f0ea9b1c925fd6cfe6ab044f1e097b72
|
|
7
|
+
data.tar.gz: b0fd64277dd46191d91681b7c3cbaf14b70b03ac80324fe9d1a01e6b2455486e2d6c6c3c2dbd214430be2867d7f5967538f6ad4a2d76ed855664c44d14c0da65
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/LLMS.txt
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# Toolchest
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) for Rails. Toolboxes are controllers, tools are actions.
|
|
4
|
+
|
|
5
|
+
## Adding a toolbox (the thing you'll do most)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
rails g toolchest:toolbox Orders show create update
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This creates three files:
|
|
12
|
+
|
|
13
|
+
- `app/toolboxes/orders_toolbox.rb` — the toolbox (like a controller)
|
|
14
|
+
- `app/views/toolboxes/orders/{show,create,update}.json.jb` — views for each tool
|
|
15
|
+
- `spec/toolboxes/orders_toolbox_spec.rb` — test file
|
|
16
|
+
|
|
17
|
+
Fill in the toolbox:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
class OrdersToolbox < ApplicationToolbox
|
|
21
|
+
before_action :set_order, only: [:show, :update]
|
|
22
|
+
|
|
23
|
+
tool "Look up an order" do
|
|
24
|
+
param :order_id, :string, "The order ID"
|
|
25
|
+
end
|
|
26
|
+
def show
|
|
27
|
+
# renders app/views/toolboxes/orders/show.json.jb automatically
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
tool "Update order status" do
|
|
31
|
+
param :order_id, :string, "The order ID"
|
|
32
|
+
param :status, :string, "New status", enum: %w[pending confirmed shipped]
|
|
33
|
+
end
|
|
34
|
+
def update
|
|
35
|
+
@order.update(params.permit(:status).to_h)
|
|
36
|
+
render :show
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
tool "Create a new order" do
|
|
40
|
+
param :customer_id, :string, "Customer"
|
|
41
|
+
end
|
|
42
|
+
def create
|
|
43
|
+
@order = Order.create!(params.permit(:customer_id).to_h)
|
|
44
|
+
render :show
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def set_order
|
|
50
|
+
@order = Order.find(params[:order_id])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Fill in the view:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# app/views/toolboxes/orders/show.json.jb
|
|
59
|
+
{
|
|
60
|
+
id: @order.id,
|
|
61
|
+
status: @order.status,
|
|
62
|
+
customer: @order.customer.name
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Test it:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
require "toolchest/rspec"
|
|
70
|
+
|
|
71
|
+
RSpec.describe OrdersToolbox, type: :toolbox do
|
|
72
|
+
it "shows an order" do
|
|
73
|
+
order = create(:order)
|
|
74
|
+
call_tool "orders_show", params: { order_id: order.id.to_s }, as: user
|
|
75
|
+
expect(tool_response).to be_success
|
|
76
|
+
expect(tool_response.text).to include(order.customer.name)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That's it. The tool is live at your MCP endpoint.
|
|
82
|
+
|
|
83
|
+
## Common patterns
|
|
84
|
+
|
|
85
|
+
### Toolbox wrapping an AR model
|
|
86
|
+
|
|
87
|
+
Use `default_param` to avoid repeating the ID param on every tool, and `before_action` to load the record:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
class OrdersToolbox < ApplicationToolbox
|
|
91
|
+
default_param :order_id, :string, "The order ID", except: [:create, :search]
|
|
92
|
+
before_action :set_order, except: [:create, :search]
|
|
93
|
+
|
|
94
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
95
|
+
render_error "Couldn't find that #{e.model.downcase}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
tool "Look up an order" do
|
|
99
|
+
end
|
|
100
|
+
def show; end # implicit render
|
|
101
|
+
|
|
102
|
+
tool "Update status" do
|
|
103
|
+
param :status, :string, "New status", enum: %w[pending confirmed shipped]
|
|
104
|
+
end
|
|
105
|
+
def update
|
|
106
|
+
if @order.update(params.permit(:status).to_h)
|
|
107
|
+
render :show
|
|
108
|
+
else
|
|
109
|
+
render_errors @order
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
def set_order = @order = Order.find(params[:order_id])
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Sampling (ask the client's LLM)
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
tool "Summarize an order" do
|
|
122
|
+
param :order_id, :string, "Order ID"
|
|
123
|
+
end
|
|
124
|
+
def summarize
|
|
125
|
+
@order = Order.find(params[:order_id])
|
|
126
|
+
summary = mcp_sample("Summarize this order", context: @order.to_json)
|
|
127
|
+
render text: summary
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Block form: `mcp_sample { |s| s.system "..."; s.user "..."; s.max_tokens 500; s.temperature 0.3 }`
|
|
132
|
+
|
|
133
|
+
Raises `Toolchest::Error` if the client doesn't support sampling.
|
|
134
|
+
|
|
135
|
+
### Progress reporting
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
items.each_with_index do |item, i|
|
|
139
|
+
process(item)
|
|
140
|
+
mcp_progress i + 1, total: items.size, message: "Processing #{item.name}"
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
No-op if no progress token from client.
|
|
145
|
+
|
|
146
|
+
### Auth checks in before_action
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class AdminToolbox < ApplicationToolbox
|
|
150
|
+
before_action :require_admin!
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
def require_admin!
|
|
154
|
+
halt error: "forbidden" unless current_user&.admin?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Nested object params
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
tool "Create order with items" do
|
|
163
|
+
param :customer_id, :string, "Customer"
|
|
164
|
+
param :items, [:object], "Line items" do
|
|
165
|
+
param :product_id, :string, "Product SKU"
|
|
166
|
+
param :quantity, :integer, "How many", default: 1
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Suggesting the next tool
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
def create
|
|
175
|
+
@order = Order.create!(...)
|
|
176
|
+
render :show
|
|
177
|
+
suggests :show, "Get the full order details"
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Setup
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
bundle add toolchest jb
|
|
185
|
+
rails g toolchest:install --auth=none # or --auth=token, --auth=oauth
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# config/routes.rb — for :none or :token
|
|
190
|
+
mount Toolchest::Engine => "/mcp"
|
|
191
|
+
|
|
192
|
+
# for :oauth — use Toolchest.app and add discovery routes
|
|
193
|
+
mount Toolchest.app => "/mcp"
|
|
194
|
+
toolchest_oauth
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Tool DSL reference
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
tool "Description for the LLM" do
|
|
201
|
+
param :name, :type, "description"
|
|
202
|
+
param :name, :type, "description", optional: true
|
|
203
|
+
param :name, :type, "description", enum: %w[a b c]
|
|
204
|
+
param :name, :type, "description", default: "value"
|
|
205
|
+
param :items, [:object], "array of objects" do
|
|
206
|
+
param :nested_field, :string, "nested"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
def method_name
|
|
210
|
+
# implementation
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Types: `:string`, `:integer`, `:number`, `:boolean`, `:object`, `[:object]`, `[:string]`, etc.
|
|
215
|
+
|
|
216
|
+
Options on `tool`: `name: "custom_tool_name"`, `access: :read` or `access: :write` (for scope filtering + annotations), `annotations: { openWorldHint: true }` (override hints).
|
|
217
|
+
|
|
218
|
+
`access: :read` → `readOnlyHint: true, destructiveHint: false`. `access: :write` → `readOnlyHint: false, destructiveHint: true`.
|
|
219
|
+
|
|
220
|
+
## Instance methods in toolbox actions
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
auth # Toolchest::AuthContext (nil for :none auth)
|
|
224
|
+
auth.resource_owner # whatever your authenticate block returns
|
|
225
|
+
auth.scopes # token scopes (always preserved)
|
|
226
|
+
auth.token # the raw token record
|
|
227
|
+
params # Toolchest::Parameters (declared keys only)
|
|
228
|
+
render :action # renders app/views/toolboxes/{toolbox}/{action}.json.jb
|
|
229
|
+
render "path/to/template" # explicit template path
|
|
230
|
+
render json: { ... } # inline JSON
|
|
231
|
+
render text: "string" # plain text
|
|
232
|
+
# no render call = implicit render using current action name
|
|
233
|
+
render_error "message" # MCP error (isError: true)
|
|
234
|
+
render_errors @record # MCP error from ActiveModel .errors
|
|
235
|
+
suggests :action, "hint" # suggest next tool call to the LLM
|
|
236
|
+
suggests "full_tool_name", "hint" # suggest by full tool name
|
|
237
|
+
mcp_log :info, "message" # MCP log notification
|
|
238
|
+
mcp_sample "prompt" # ask client's LLM, returns text
|
|
239
|
+
mcp_sample { |s| s.user "..." } # block form with system, max_tokens, temperature
|
|
240
|
+
mcp_progress 3, total: 10 # report progress (optional message:)
|
|
241
|
+
halt error: "forbidden" # stop execution, render error
|
|
242
|
+
halt # stop execution (must have rendered already)
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Auth
|
|
246
|
+
|
|
247
|
+
Three built-in strategies. Default is `:none`.
|
|
248
|
+
|
|
249
|
+
### :none
|
|
250
|
+
|
|
251
|
+
No auth. `auth` is nil. All tools visible.
|
|
252
|
+
|
|
253
|
+
### :token
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
Toolchest.configure do |config|
|
|
257
|
+
config.auth = :token
|
|
258
|
+
config.authenticate do |token|
|
|
259
|
+
# token is EnvTokenRecord (owner_id, scopes) or Toolchest::Token AR record
|
|
260
|
+
# return value becomes auth.resource_owner in toolboxes
|
|
261
|
+
User.find(token.owner_id)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Dev setup (env vars, no DB):
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
TOOLCHEST_TOKEN=tcht_dev_secret
|
|
270
|
+
TOOLCHEST_TOKEN_OWNER=user:1
|
|
271
|
+
TOOLCHEST_TOKEN_SCOPES="orders:read orders:write"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Production (DB tokens):
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
rails g toolchest:auth token && rails db:migrate
|
|
278
|
+
rails toolchest:token:generate OWNER=user:1 NAME="claude desktop"
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### :oauth
|
|
282
|
+
|
|
283
|
+
Full OAuth 2.1 with PKCE and DCR. Isolated from host app auth.
|
|
284
|
+
|
|
285
|
+
```ruby
|
|
286
|
+
Toolchest.configure do |config|
|
|
287
|
+
config.auth = :oauth
|
|
288
|
+
config.login_path = "/login"
|
|
289
|
+
|
|
290
|
+
config.current_user_for_oauth do |request|
|
|
291
|
+
request.env["warden"]&.user # for consent screen
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
config.authenticate do |token|
|
|
295
|
+
# token is Toolchest::OauthAccessToken (resource_owner_id, scopes, mount_key)
|
|
296
|
+
# scopes are always preserved — returning a User here is fine
|
|
297
|
+
User.find(token.resource_owner_id)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Custom auth
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
config.auth = MyAuth.new
|
|
306
|
+
|
|
307
|
+
class MyAuth < Toolchest::Auth::Base
|
|
308
|
+
def authenticate(request)
|
|
309
|
+
key = request.env["HTTP_X_API_KEY"]
|
|
310
|
+
user = ApiKey.active.find_by(key: key)&.owner
|
|
311
|
+
return nil unless user
|
|
312
|
+
Toolchest::AuthContext.new(resource_owner: user, scopes: [], token: nil)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### AuthContext
|
|
318
|
+
|
|
319
|
+
`auth` always returns a `Toolchest::AuthContext` (or nil for `:none`):
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
auth.resource_owner # whatever your authenticate block returns (User, etc.)
|
|
323
|
+
auth.scopes # array of scope strings from the token — never lost
|
|
324
|
+
auth.token # the raw token record
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The generator creates `def current_user = auth&.resource_owner` in ApplicationToolbox.
|
|
328
|
+
|
|
329
|
+
Scopes are always extracted from the token *before* the authenticate block runs. You cannot accidentally lose scopes by returning a User from authenticate.
|
|
330
|
+
|
|
331
|
+
## Scopes
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
config.scopes = {
|
|
335
|
+
"orders:read" => "View order details",
|
|
336
|
+
"orders:write" => "Create and modify orders"
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Pattern: `{toolbox}:{access}`. Actions named `show`, `index`, `list`, `search` default to `:read`, everything else to `:write`. Override per-tool: `tool "desc", access: :read`.
|
|
341
|
+
|
|
342
|
+
`write` scope grants both read and write. Bare scope (e.g. `orders`) grants full access.
|
|
343
|
+
|
|
344
|
+
Scopes filter `tools/list` — clients only see tools their token allows. Fails closed: if scopes can't be determined, no tools are shown.
|
|
345
|
+
|
|
346
|
+
Disable: `config.filter_tools_by_scope = false`
|
|
347
|
+
|
|
348
|
+
### Optional scopes (checkboxes on consent)
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
config.optional_scopes = true # users can uncheck scopes (default: false)
|
|
352
|
+
config.required_scopes = ["orders:read"] # always granted, can't uncheck (default: [])
|
|
353
|
+
config.allowed_scopes_for do |user, requested| # per-user gating (default: show all)
|
|
354
|
+
user.admin? ? requested : requested - ["admin:write"]
|
|
355
|
+
end
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Resources and prompts
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
class OrdersToolbox < ApplicationToolbox
|
|
362
|
+
resource "orders://schema", name: "Order schema", description: "Fields" do
|
|
363
|
+
{ fields: Order.column_names }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
resource "orders://{order_id}", name: "Order", description: "Order by ID" do |order_id:|
|
|
367
|
+
Order.find(order_id).as_json
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
prompt "debug-order", description: "Debug an order",
|
|
371
|
+
arguments: { order_id: { type: :string, required: true } } do |order_id:|
|
|
372
|
+
order = Order.find(order_id)
|
|
373
|
+
[{ role: "user", content: "Debug this order:\n#{order.to_json}" }]
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## Multi-mount
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
Toolchest.configure { |c| c.auth = :oauth; c.toolbox_module = "Public" }
|
|
382
|
+
Toolchest.configure(:admin) { |c| c.auth = :token; c.toolbox_module = "Admin" }
|
|
383
|
+
|
|
384
|
+
# routes.rb
|
|
385
|
+
mount Toolchest.app => "/mcp"
|
|
386
|
+
mount Toolchest.app(:admin) => "/admin-mcp"
|
|
387
|
+
toolchest_oauth default_mount: :default
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Tool naming
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
config.tool_naming = :underscored # orders_show (default)
|
|
394
|
+
config.tool_naming = :dotted # orders.show
|
|
395
|
+
config.tool_naming = :slashed # orders/show
|
|
396
|
+
config.tool_naming = ->(prefix, method) { "#{prefix}__#{method}" }
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Per-tool: `tool "desc", name: "custom_name"`
|
|
400
|
+
|
|
401
|
+
Namespaced toolboxes (`Admin::OrdersToolbox`) produce `admin_orders_{action}`.
|
|
402
|
+
|
|
403
|
+
## Testing
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
require "toolchest/rspec"
|
|
407
|
+
|
|
408
|
+
RSpec.describe OrdersToolbox, type: :toolbox do
|
|
409
|
+
it "shows an order" do
|
|
410
|
+
call_tool "orders_show", params: { order_id: "123" }, as: user
|
|
411
|
+
expect(tool_response).to be_success
|
|
412
|
+
expect(tool_response.text).to include("shipped")
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it "handles errors" do
|
|
416
|
+
call_tool "orders_show", params: { order_id: "missing" }
|
|
417
|
+
expect(tool_response).to be_error
|
|
418
|
+
expect(tool_response).to include_text("not found")
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
it "suggests next tool" do
|
|
422
|
+
call_tool "orders_create", params: { customer_id: "c1" }, as: user
|
|
423
|
+
expect(tool_response).to suggest("orders_show")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Matchers: `be_success`, `be_error`, `include_text("str")`, `suggest("tool_name")`
|
|
429
|
+
|
|
430
|
+
## Generators
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
rails g toolchest:install # initializer, ApplicationToolbox, routes
|
|
434
|
+
rails g toolchest:install --auth=oauth # same + OAuth migration
|
|
435
|
+
rails g toolchest:toolbox Orders show # toolbox + views + spec
|
|
436
|
+
rails g toolchest:auth token # token migration
|
|
437
|
+
rails g toolchest:auth oauth # OAuth migration
|
|
438
|
+
rails g toolchest:consent # eject consent view
|
|
439
|
+
rails g toolchest:oauth_views # eject all OAuth views + controllers
|
|
440
|
+
rails g toolchest:skills # install Claude Code slash commands
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
## Rake tasks
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
rails toolchest:tools # list all registered tools
|
|
447
|
+
rails toolchest:token:generate OWNER=user:1 # create a token
|
|
448
|
+
rails toolchest:token:list # list tokens
|
|
449
|
+
rails toolchest:token:revoke TOKEN=tcht_... # revoke a token
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Error classes
|
|
453
|
+
|
|
454
|
+
- `Toolchest::ParameterMissing` — raised by `params.require(:key)`
|
|
455
|
+
- `Toolchest::MissingTemplate` — raised when implicit render can't find a view
|
|
456
|
+
- `Toolchest::Error` — base error class
|
|
457
|
+
|
|
458
|
+
Handle with `rescue_from` in your toolbox.
|
|
459
|
+
|
|
460
|
+
## File locations
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
app/toolboxes/application_toolbox.rb base class
|
|
464
|
+
app/toolboxes/orders_toolbox.rb toolbox
|
|
465
|
+
app/toolboxes/admin/orders_toolbox.rb namespaced toolbox
|
|
466
|
+
app/views/toolboxes/orders/show.json.jb view
|
|
467
|
+
app/views/toolboxes/admin/orders/show.json.jb namespaced view
|
|
468
|
+
spec/toolboxes/orders_toolbox_spec.rb spec
|
|
469
|
+
config/initializers/toolchest.rb config
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Server config
|
|
473
|
+
|
|
474
|
+
```ruby
|
|
475
|
+
Toolchest.configure do |config|
|
|
476
|
+
config.server_name = "My App"
|
|
477
|
+
config.server_description = "Order management tools"
|
|
478
|
+
config.server_instructions = "Always look up the customer before modifying orders."
|
|
479
|
+
end
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
## Completion
|
|
483
|
+
|
|
484
|
+
Params with `enum:` automatically power `completion/complete`. No extra code needed.
|