Repository navigation
Describe ActiveAdmin forms over MCP with a describe_form tool - #14
Merged
Merged
Conversation
Phase 3 of the actions-and-forms design (#8), and the last of them. A client calling create or update had to guess field types and allowed values from column names. describe_form reads the resource's own `form do ... end` block when it declares one, reporting each input's as:, label:, hint: and, when the collection: is a literal array, its allowed values. Most resources declare no form block. ActiveAdmin renders a bare `f.inputs` for those, which Formtastic only expands at render time, so there is nothing to introspect; the description is derived from permit_params instead, which is what create and update enforce anyway. The payload says which source it used. Either way each field is annotated from the model with its column type and whether the model validates its presence. FormFieldCollector returns, rebuilt for this rather than for the bare names phase 2 removed the last consumer of. It now records input options and reports has_many blocks as nested groups instead of skipping them. A collection: that is a relation or a proc is omitted rather than evaluated: a description should not fire a query, and a relation can be arbitrarily large. action: selects the gate, not the shape, since ActiveAdmin uses one form block for both. Describing a form the user could never submit discloses the shape of a resource they cannot write, so "new" requires create and "edit" requires update, refused with the messages those tools already use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Phase 3 of the design in #8 — form description, and the last of the three phases.
The gap
A client calling
createorupdatehad to guess field types and allowed values from column names.FormFieldCollectorrecorded only flat input names, and phase 2 removed its last consumer.What this does
describe_form(resource:, action: "new" | "edit"). Reads the resource's ownform do … endblock when it declares one, reporting each input'sas:,label:,hint:and — when thecollection:is a literal array — its allowed values.Resources with no form block get a derived description. ActiveAdmin renders a bare
f.inputsfor those, which Formtastic only expands at render time, so there is genuinely nothing to introspect. The description falls back to the resource'spermit_params, which is whatcreateandupdateenforce anyway. The payload'ssourcesays which of the two you are reading.Either way, every field is annotated from the model with the column type it is stored in and whether the model validates its presence.
has_manyblocks are reported undernestedrather than flattened in with the record's own fields.FormFieldCollectorreturns, rebuilt for this purpose rather than for the bare names phase 2 removed the last consumer of. It records input options, recurses intohas_manyblocks, and still drops presentation-only options (input_html:,wrapper_html:) that say nothing an MCP client can act on.Two deliberate limits
collection:that is anActiveRecord::Relationor a proc is omitted rather than evaluated. Describing a form should not fire a query, and a relation can be arbitrarily large. An action'ssuggestions:remains the way to offer dynamic values.permit_paramsomits is described and then silently dropped on write. Fixing it means reaching intoRecordWriter's permit resolution, and it cannot arise on thepermit_paramsfallback path. Happy to add it if you'd rather.Authorization
action:selects the gate, not the shape — ActiveAdmin uses one form block for both."new"requires the resource to registercreateand passcreateauthorization;"edit"requiresupdate. Describing a form the user could never submit tells them nothing actionable and discloses the shape of a resource they cannot write, so it is refused with the same messagescreateandupdatealready use.Testing
FormDescriptionspec against the real ActiveAdmin harness — which gains aShiftresource declaring an explicit form block (with ahas_many), so both source paths and the nested case are covered.Reviewfixture resource is the only one declaring a form block, so the declared-form path is exercised end to end;Postcovers thepermit_paramsfallback, andAuthor/Tagthe two refusals.Tagapermit_params, adding a form block toPost, makingAuthorcreatable, wideningPost's permitted params and dropping its validation. Each targeted example failed; fixtures restored and both suites re-run green.(One incidental find: the fixture resource was originally
Comment, which collides with ActiveAdmin's own built-inActiveAdmin::Commentand fails the app build withConfigMismatch. Renamed toReview.)README gains a "Describing a form" section and a tool-table row; CHANGELOG records the addition.
🤖 Generated with Claude Code