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
data/README.md ADDED
@@ -0,0 +1,572 @@
1
+ # Toolchest
2
+
3
+ > **Research preview** — APIs may change, some features aren't great yet, and it is not yet recommended for production use (i'm still gonna though!). Feedback and bug reports are welcome.
4
+
5
+ Every Ruby MCP library I could find is Ruby from an MCP perspective. Toolchest is MCP from a Rails perspective.
6
+
7
+ Toolboxes are controllers, tools are actions.
8
+
9
+ ## Why
10
+
11
+ Every Ruby MCP gem I found treats tools as isolated service objects. One file per tool, set_model reimplemented in every `call` method, a whole DSL that exists nowhere else in your app. Four tools for orders means four files.
12
+
13
+ A tool call *is* a controller action. Authenticated request, named action, structured params, do the thing, return JSON. Rails figured this out twenty years ago.
14
+
15
+ Toolboxes are controllers. Tools are actions. `before_action` works. `rescue_from` works. Views are views.
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ bundle add toolchest jb
21
+ rails g toolchest:install --auth=none
22
+ rails g toolchest Orders show create
23
+ rails s
24
+ # point your MCP client at http://localhost:3000/mcp
25
+ ```
26
+
27
+ ## Toolboxes
28
+
29
+ `app/toolboxes/`. They work like controllers because they basically are.
30
+
31
+ ```ruby
32
+ # app/toolboxes/application_toolbox.rb
33
+ class ApplicationToolbox < Toolchest::Toolbox
34
+ def current_user = auth&.resource_owner
35
+
36
+ rescue_from ActiveRecord::RecordNotFound do |e|
37
+ render_error "Couldn't find that #{e.model.downcase}"
38
+ end
39
+ end
40
+ ```
41
+
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, add shared error handling, include your gems.
43
+
44
+ ```ruby
45
+ # app/toolboxes/orders_toolbox.rb
46
+ class OrdersToolbox < ApplicationToolbox
47
+ default_param :order_id, :string, "The order ID", except: [:create, :search]
48
+ before_action :set_order, except: [:create, :search]
49
+
50
+ tool "Look up an order by ID" do
51
+ end
52
+ def show
53
+ # implicit render: app/views/toolboxes/orders/show.json.jb
54
+ end
55
+
56
+ tool "Update order status" do
57
+ param :status, :string, "New status", enum: %w[pending confirmed shipped]
58
+ param :tracking, :string, "Tracking number", optional: true
59
+ end
60
+ def update
61
+ if @order.update(params.permit(:status, :tracking).to_h)
62
+ render :show
63
+ else
64
+ render_errors @order
65
+ end
66
+ end
67
+
68
+ tool "Create a new order" do
69
+ param :customer_id, :string, "Customer"
70
+ param :items, [:object], "Line items" do
71
+ param :product_id, :string, "Product SKU"
72
+ param :quantity, :integer, "How many", default: 1
73
+ end
74
+ end
75
+ def create
76
+ @order = Order.new(params.permit(:customer_id).to_h)
77
+ if @order.save
78
+ render :show
79
+ suggests :show, "Get the full order details"
80
+ else
81
+ render_errors @order
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def set_order
88
+ @order = Order.find(params[:order_id])
89
+ end
90
+ end
91
+ ```
92
+
93
+ ### halt
94
+
95
+ Stop execution early, usually in a `before_action`:
96
+
97
+ ```ruby
98
+ before_action :require_admin!
99
+
100
+ def require_admin!
101
+ halt error: "forbidden" unless current_user.admin?
102
+ end
103
+ ```
104
+
105
+ ### Rendering
106
+
107
+ ```ruby
108
+ render :show # app/views/toolboxes/{toolbox}/show.json.jb
109
+ render "shared/status" # explicit template path
110
+ render json: { ok: true } # inline, no view file
111
+ render text: "done" # plain text
112
+ # no render call = implicit render of current action name
113
+ render_error "Something broke" # MCP error (isError: true), string
114
+ render_errors @order # MCP error from ActiveModel errors
115
+ ```
116
+
117
+ ### Views
118
+
119
+ `app/views/toolboxes/`, using [jb](https://github.com/amatsuda/jb) or jbuilder:
120
+
121
+ ```ruby
122
+ # app/views/toolboxes/orders/show.json.jb
123
+ {
124
+ id: @order.id,
125
+ status: @order.status,
126
+ customer: @order.customer.name,
127
+ total: @order.total.to_f
128
+ }
129
+ ```
130
+
131
+ `render :show` after mutations. Most toolboxes need one or two views.
132
+
133
+ ### Resources and prompts
134
+
135
+ Supported, though most toolboxes won't need them.
136
+
137
+ ```ruby
138
+ class OrdersToolbox < ApplicationToolbox
139
+ resource "orders://schema", name: "Order schema", description: "Order field structure" do
140
+ { fields: Order.column_names, statuses: Order::STATUSES }
141
+ end
142
+
143
+ resource "orders://{order_id}", name: "Order details", description: "Full order by ID" do |order_id:|
144
+ Order.find(order_id).as_json(include: :items)
145
+ end
146
+
147
+ prompt "debug-order", description: "Investigate order issues",
148
+ arguments: { order_id: { type: :string, required: true } } do |order_id:|
149
+ order = Order.find(order_id)
150
+ [{ role: "user", content: "Debug this order:\n#{order.to_json}" }]
151
+ end
152
+ end
153
+ ```
154
+
155
+ ### Suggests
156
+
157
+ Tell the LLM what to call next:
158
+
159
+ ```ruby
160
+ suggests :show, "Call orders_show for full details"
161
+ ```
162
+
163
+ ### Params
164
+
165
+ Like `ActionController::Parameters`:
166
+
167
+ ```ruby
168
+ params[:order_id]
169
+ params.require(:order_id) # raises Toolchest::ParameterMissing if absent
170
+ params.permit(:status, :tracking)
171
+ params.slice(:status)
172
+ params.except(:internal_field)
173
+ ```
174
+
175
+ Params are automatically filtered to only keys declared in the tool's `param` block. Undeclared keys get dropped.
176
+
177
+ ### Sampling
178
+
179
+ Ask the client's LLM to do work from inside a tool action:
180
+
181
+ ```ruby
182
+ tool "Summarize an order" do
183
+ param :order_id, :string, "Order ID"
184
+ end
185
+ def summarize
186
+ @order = Order.find(params[:order_id])
187
+ summary = mcp_sample("Summarize this order for a support agent", context: @order.to_json)
188
+ render text: summary
189
+ end
190
+ ```
191
+
192
+ Block form for more control:
193
+
194
+ ```ruby
195
+ summary = mcp_sample do |s|
196
+ s.system "You are a support analyst"
197
+ s.user "Analyze this order:\n#{@order.to_json}"
198
+ s.max_tokens 500
199
+ s.temperature 0.3
200
+ end
201
+ ```
202
+
203
+ Raises `Toolchest::Error` if the client doesn't support sampling. Handle it with `rescue_from` in your toolbox.
204
+
205
+ ### Progress
206
+
207
+ Report progress during long-running actions. Clients that support it show a progress bar:
208
+
209
+ ```ruby
210
+ tool "Import customers" do
211
+ param :file_url, :string, "CSV URL"
212
+ end
213
+ def import
214
+ rows = CSV.parse(download(params[:file_url]))
215
+ rows.each_with_index do |row, i|
216
+ Customer.create!(row.to_h)
217
+ mcp_progress i + 1, total: rows.size, message: "Importing #{row[:name]}"
218
+ end
219
+ render text: "Imported #{rows.size} customers"
220
+ end
221
+ ```
222
+
223
+ No-op if the client doesn't send a progress token.
224
+
225
+ ### Annotations
226
+
227
+ Tool annotations tell the client about a tool's behavior. They're derived automatically from `access:`:
228
+
229
+ ```ruby
230
+ tool "Show order", access: :read do # → readOnlyHint: true, destructiveHint: false
231
+ end
232
+
233
+ tool "Delete order", access: :write do # → readOnlyHint: false, destructiveHint: true
234
+ end
235
+ ```
236
+
237
+ Override or add hints with `annotations:`:
238
+
239
+ ```ruby
240
+ tool "Export data", access: :read, annotations: { openWorldHint: true } do
241
+ end
242
+ ```
243
+
244
+ ### Logging
245
+
246
+ ```ruby
247
+ mcp_log :info, "Processing order #{@order.id}"
248
+ ```
249
+
250
+ ### Completion
251
+
252
+ If a param has `enum:`, those values automatically power MCP's `completion/complete`. Clients that support autocomplete get it for free.
253
+
254
+ ### Server instructions
255
+
256
+ Tell the LLM how to use your tools:
257
+
258
+ ```ruby
259
+ Toolchest.configure do |config|
260
+ config.server_instructions = "You are a support agent. Always look up the customer before modifying orders."
261
+ end
262
+ ```
263
+
264
+ This shows up in the MCP initialize response. `server_name` and `server_description` are also available.
265
+
266
+ ## Auth
267
+
268
+ Three built-in strategies, or bring your own. Default is `:none`.
269
+
270
+ ### :token
271
+
272
+ Bearer tokens. In dev, set env vars and you're done:
273
+
274
+ ```bash
275
+ TOOLCHEST_TOKEN=tcht_dev_secret
276
+ TOOLCHEST_TOKEN_OWNER=user:1
277
+ TOOLCHEST_TOKEN_SCOPES="orders:read orders:write" # optional
278
+ ```
279
+
280
+ For production, run the migration and manage with rake:
281
+
282
+ ```bash
283
+ rails g toolchest:auth token
284
+ rails db:migrate
285
+ rails toolchest:token:generate OWNER=user:1 NAME="claude desktop"
286
+ rails toolchest:token:list
287
+ rails toolchest:token:revoke TOKEN=tcht_...
288
+ ```
289
+
290
+ ```ruby
291
+ Toolchest.configure do |config|
292
+ config.auth = :token
293
+
294
+ config.authenticate do |token|
295
+ User.find(token.owner_id)
296
+ end
297
+ end
298
+ ```
299
+
300
+ `authenticate` resolves the token to a user (or anything). The return value becomes `auth.resource_owner` in your toolboxes. Scopes are preserved from the token automatically — you can't lose them here.
301
+
302
+ ### :oauth
303
+
304
+ MCP clients like Claude Desktop and Cursor need OAuth 2.1 with PKCE and Dynamic Client Registration. Toolchest ships a built-in OAuth provider so you can get this working without wiring up Doorkeeper.
305
+
306
+ It's intentionally minimal — enough to make MCP auth work, completely isolated from the rest of your app. Its tables are all `toolchest_`-prefixed, it doesn't know Doorkeeper exists. It will not break your existing OAuth setup.
307
+
308
+ If you already have an OAuth provider, you probably want `:token` instead and validate your own tokens in the `authenticate` block. The built-in provider exists so you don't have to figure all that out before you can connect Claude Desktop.
309
+
310
+ ```bash
311
+ rails g toolchest:install --auth=oauth
312
+ rails db:migrate
313
+ ```
314
+
315
+ ```ruby
316
+ Toolchest.configure do |config|
317
+ config.auth = :oauth
318
+ config.login_path = "/login"
319
+
320
+ config.current_user_for_oauth do |request|
321
+ # return the logged-in user for the consent screen, or nil to redirect
322
+ request.env["warden"]&.user # devise example
323
+ end
324
+
325
+ config.authenticate do |token|
326
+ User.find(token.resource_owner_id)
327
+ end
328
+ end
329
+ ```
330
+
331
+ `authenticate` resolves the token to `auth.resource_owner`. Scopes come from the token and are never lost, even if you return a plain User.
332
+
333
+ You also need `toolchest_oauth` in your routes for `.well-known` discovery:
334
+
335
+ ```ruby
336
+ # config/routes.rb
337
+ mount Toolchest.app => "/mcp"
338
+ toolchest_oauth
339
+ ```
340
+
341
+ This adds the endpoints MCP clients expect:
342
+
343
+ ```
344
+ /.well-known/oauth-authorization-server ← discovery (app root)
345
+ /.well-known/oauth-protected-resource ← discovery (app root)
346
+ /mcp/oauth/authorize ← consent screen
347
+ /mcp/oauth/token ← token exchange
348
+ /mcp/oauth/register ← dynamic client registration
349
+ ```
350
+
351
+ Customize the consent view: `rails g toolchest:consent`
352
+
353
+ There's a built-in "connected applications" page at `/mcp/oauth/authorized_applications` where users can revoke access. Link to it from your account settings.
354
+
355
+ You can also query tokens directly:
356
+
357
+ ```ruby
358
+ Toolchest::OauthAccessToken.revoke_all_for(app, user.id)
359
+ Toolchest::OauthAccessGrant.revoke_all_for(app, user.id)
360
+ app.destroy # cascades to all grants and tokens
361
+ ```
362
+
363
+ ### Custom
364
+
365
+ If the built-in strategies don't fit, pass any object that responds to `#authenticate(request)`:
366
+
367
+ ```ruby
368
+ Toolchest.configure do |config|
369
+ config.auth = WardenAuth.new
370
+ end
371
+ ```
372
+
373
+ ```ruby
374
+ class WardenAuth
375
+ def authenticate(request)
376
+ user = request.env["warden"]&.user
377
+ return nil unless user
378
+ Toolchest::AuthContext.new(resource_owner: user, scopes: [], token: nil)
379
+ end
380
+ end
381
+ ```
382
+
383
+ Custom strategies return an `AuthContext` (or nil for unauthenticated). If you return something else, `auth` will be that object directly — but scope filtering only works with `AuthContext`.
384
+
385
+ You can inherit from `Toolchest::Auth::Base` to get `extract_bearer_token` for free:
386
+
387
+ ```ruby
388
+ class ApiKeyAuth < Toolchest::Auth::Base
389
+ def authenticate(request)
390
+ key = request.env["HTTP_X_API_KEY"]
391
+ ApiKey.active.find_by(key: key)&.owner
392
+ end
393
+ end
394
+ ```
395
+
396
+ ## Scopes
397
+
398
+ Scopes work with both `:token` and `:oauth` auth. Define them in your config:
399
+
400
+ ```ruby
401
+ config.scopes = {
402
+ "orders:read" => "View order details",
403
+ "orders:write" => "Create and modify orders",
404
+ "users:read" => "View user profiles"
405
+ }
406
+ ```
407
+
408
+ The pattern is `{toolbox}:{access}`. Toolchest maps tools to scopes automatically: actions named `show`, `index`, `list`, or `search` are `:read`, everything else is `:write`. A client granted `orders:read` sees `orders_show` and `orders_search` but not `orders_cancel`. `orders:write` gets everything. `orders` with no suffix also gets everything.
409
+
410
+ With OAuth, scopes show up on the consent screen and filter `tools/list` by what was granted. With token auth, set scopes via `TOOLCHEST_TOKEN_SCOPES` (env var) or the `scopes` column on the token record.
411
+
412
+ Override when the convention is wrong:
413
+
414
+ ```ruby
415
+ tool "Export data", access: :read do
416
+ end
417
+ def export
418
+ # ...
419
+ end
420
+ ```
421
+
422
+ Turn it off: `config.filter_tools_by_scope = false`
423
+
424
+ ### Optional scopes (checkboxes)
425
+
426
+ By default, the consent screen is all-or-nothing — approve all requested scopes or deny. Enable `optional_scopes` and users get checkboxes:
427
+
428
+ ```ruby
429
+ config.optional_scopes = true
430
+ ```
431
+
432
+ All scopes start checked. Users uncheck what they don't want. The token only gets the scopes the user approved. That's it — no other config needed.
433
+
434
+ Layer on more control when you need it:
435
+
436
+ ```ruby
437
+ # These scopes are always granted (checked + disabled on the consent screen)
438
+ config.required_scopes = ["orders:read"]
439
+
440
+ # Per-user gating — hide scopes from users who shouldn't grant them
441
+ config.allowed_scopes_for do |user, requested_scopes|
442
+ user.admin? ? requested_scopes : requested_scopes - ["admin:write"]
443
+ end
444
+ ```
445
+
446
+ Scopes hidden by `allowed_scopes_for` never appear on the consent screen and can't be granted even if the POST is tampered with.
447
+
448
+ ## Multi-mount
449
+
450
+ Separate MCP endpoints, different auth, different toolboxes:
451
+
452
+ ```ruby
453
+ Toolchest.configure do |config|
454
+ config.auth = :oauth
455
+ config.toolbox_module = "Public"
456
+ end
457
+
458
+ Toolchest.configure(:admin) do |config|
459
+ config.auth = :token
460
+ config.toolbox_module = "Admin"
461
+ end
462
+ ```
463
+
464
+ ```ruby
465
+ # config/routes.rb
466
+ mount Toolchest.app => "/mcp"
467
+ mount Toolchest.app(:admin) => "/admin-mcp"
468
+ toolchest_oauth
469
+ ```
470
+
471
+ Namespace your toolboxes under modules (`Admin::OrdersToolbox`, `Public::OrdersToolbox`) and they route to the right mount.
472
+
473
+ With multiple OAuth mounts, `.well-known` discovery uses the path suffix per [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) — e.g. `/.well-known/oauth-authorization-server/admin-mcp`. Some clients (notably Cursor) hit the bare path without a suffix. Set `default_mount` so Toolchest knows which mount to use:
474
+
475
+ ```ruby
476
+ toolchest_oauth default_mount: :default
477
+ ```
478
+
479
+ With a single OAuth mount this isn't needed.
480
+
481
+ ## Tool naming
482
+
483
+ ```ruby
484
+ config.tool_naming = :underscored # orders_show (default)
485
+ config.tool_naming = :dotted # orders.show
486
+ config.tool_naming = :slashed # orders/show
487
+ config.tool_naming = ->(prefix, method) { "#{prefix}__#{method}" }
488
+ ```
489
+
490
+ Per-tool: `tool "description", name: "custom_name"`
491
+
492
+ ## Generators
493
+
494
+ ```bash
495
+ rails g toolchest:install # initializer, app toolbox, routes
496
+ rails g toolchest Orders show create # toolbox + views + spec
497
+ rails g toolchest Admin::Orders show # namespaced
498
+ rails g toolchest:auth oauth # add auth migration + views
499
+ rails g toolchest:consent # eject consent view
500
+ rails g toolchest:oauth_views # eject all OAuth views + controllers
501
+ rails g toolchest:skills # install Claude Code slash commands
502
+ ```
503
+
504
+ ## Introspection
505
+
506
+ ```bash
507
+ rails toolchest:tools
508
+ ```
509
+
510
+ ## Testing
511
+
512
+ ```ruby
513
+ RSpec.describe OrdersToolbox, type: :toolbox do
514
+ it "shows an order" do
515
+ call_tool "orders_show", params: { order_id: "123" }, as: user
516
+ expect(tool_response).to be_success
517
+ expect(tool_response.text).to include("shipped")
518
+ end
519
+
520
+ it "returns errors for invalid updates" do
521
+ call_tool "orders_update", params: { order_id: "123", status: "pending" }, as: user
522
+ expect(tool_response).to be_error
523
+ end
524
+
525
+ it "suggests next tool after create" do
526
+ call_tool "orders_create", params: { customer_id: "c1" }, as: user
527
+ expect(tool_response).to suggest("orders_show")
528
+ end
529
+ end
530
+ ```
531
+
532
+ `require "toolchest/rspec"` in your `rails_helper.rb`.
533
+
534
+ ## Security notes
535
+
536
+ - **Rate limiting**: Toolchest doesn't include rate limiting. Use [rack-attack](https://github.com/rack/rack-attack) or your reverse proxy to protect token and registration endpoints.
537
+ - **HTTPS**: OAuth endpoints should always run behind TLS in production.
538
+
539
+ ## Internals
540
+
541
+ Transport is the [MCP Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk) (`mcp` gem).
542
+
543
+ OAuth provider is cribbed from [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper). Same table layout, same controller shapes. Not a dependency, just stole the design.
544
+
545
+ ## For agents
546
+
547
+ If you're implementing this with an agent (or you're the agent reading this), consider the contents of [LLMS.txt](LLMS.txt).
548
+
549
+ ## Claude Code skills
550
+
551
+ Install skills for Claude Code that know how to work with toolchest:
552
+
553
+ ```bash
554
+ rails g toolchest:skills
555
+ ```
556
+
557
+ This gives you `/add-toolbox`, `/add-tool`, and `/toolchest-auth` slash commands.
558
+
559
+ ## Requirements
560
+
561
+ - Ruby >= 3.2
562
+ - Rails >= 7.0
563
+ - [jb](https://github.com/amatsuda/jb) (recommended) or jbuilder
564
+
565
+ ## Disclaimer
566
+
567
+ This is slop by LLMs for LLMs and only a fool would use it in production. However, I am a fool.
568
+ No implied warranty of any kind, if you trust this and it explodes you please cry to someone else.
569
+
570
+ ## License
571
+
572
+ MIT