Cookbook
Task-oriented recipes: I want to do X, what do I write?
Task-oriented recipes. The README is the reference โ this is "I want to do X, what do I write?"
Every recipe is a complete, copy-pasteable snippet. They build on the handler-example app (a recruiting-workflow server, not yet published as a standalone repo) with three roles โ viewer (view_workflows), operator (manage_workflows), and manager (manage_workflows + backward_routing).
The mental model, in one line: your RBS type annotations are the authorization policy. You don't reject requests โ you compile a different schema per user, and the gem enforces it on the way in and on the way out.
- 1. Expose your first tool (read-only, end to end)
You have a Rails app and want one MCP tool that returns data. No auth subtleties yet.
- 2. Hide an entire tool behind a permission
Only users who can manage_workflows should even see the advance_step tool. Everyone else gets a clean tool list with no hint it exists.
- 3. Hide a tool behind an account feature, not a role
Visibility shouldn't depend on the user's role but on whether their account has SMS provisioned. RBAC is the wrong axis.
- 4. Hide one input field from some users
advance_step should let managers jump an applicant to any stage via a stage_id param โ but operators shouldn't even see that the parameter exists.
- 5. Give privileged users a richer output shape
When a manager reroutes an applicant, the response should include previous_stage and an audit_trail. Operators should never receive those fields โ not even i...
- 6. Pre-fill a field from the current user
Most users operate on one default workflow. You want the schema's default for workflow_id to be that user's default, not a hardcoded constant.
- 7. Validate input with constraints
You want the schema to enforce shape โ a pattern on an ID, a length range on a reason, a numeric bound โ so the LLM gets it right the first time and bad inpu...
- 8. Share a type across handlers
Every tool returns the same error shape and the same applicant record. You don't want to redeclare them in each handler.
- 9. Model success-or-error as a discriminated union
You want the LLM to reliably tell a success response from an error and branch on it โ like a TypeScript discriminated union.
- 10. Require a field only when another is present
reason is meaningless without stage_id. You want "if stage_id is provided, reason becomes required" โ without making either unconditionally required.
- 11. Change the tool description by role
The same tool does more for managers. You want its one-line description to reflect that, so the LLM understands the fuller capability when it's available.
- 12. Serve different tool surfaces from one app (domains)
One Rails app, but you want a recruiting tool surface and an operations tool surface at different URLs, each exposing its own set of tools.
- 13. Invent your own predicate vocabulary
@requires (RBAC) and @feature (account flags) aren't enough. You want to gate on plan tier, beta enrollment, A/B bucket โ whatever your domain needs.
- 14. See exactly what a role sees (debugging)
You added @requires tags and want to confirm an operator really can't see stage_id โ without wiring up a client and forging auth headers.
- 15. Mark a tool read-only / destructive (annotation hints)
You want to advertise behavioral hints โ this tool only reads, that one might delete โ so clients can warn users or auto-approve safe calls.
- Scaling a large tool surface
The two recipes below are about cost, not authorization: what happens when one domain grows past what a model can choose from well, and past what you want to...
- 16. Group a large domain into facades
A domain has grown to a hundred-plus tools. tools/list is dominated by call-time argument schemas for tools the model will never pick, and choosing from a fl...
- 17. Cache the tools/list response
tools/list compiles a schema for every visible tool, per caller. tools/call already compiles only the tool it invokes, and lifecycle methods compile nothing...
- 18. Generate tools instead of writing them
One tool per controller action, or per row in a config table, or per endpoint in a family you already ship. Hand-writing a file each is copying a declaration...
- Talking to the outside world
Every recipe so far returned a canned hash. Real handlers do I/O โ they query databases and open sockets. The gem doesn't get in the way: a handler is a plai...
- 19. Query a database from a tool
search_applicants should hit the real database, filter by what the LLM asked for, and return only the columns this user is allowed to see.
- 20. Talk to a TCP socket from a tool
A tool should open a raw TCP connection to a service, send a probe, read the reply, and report latency โ without hanging the request thread or becoming an SS...
- Where to go next
Collected from COOKBOOK.md in the repository. Edit it there, not here.