Repository navigation
Expose actions declared in shared concerns with an mcp_action DSL - #17
Merged
Merged
Conversation
Closes #16. An action opts in to MCP by carrying an mcp: key on its own declaration, which reaches nothing declared in a shared concern or by another gem. Even where the concern could be edited, one mcp: key there would describe every resource that includes it identically, which is the opposite of what is wanted. Such actions are already registered on the resource config as ordinary ControllerAction and BatchAction objects, so only the metadata was missing. The resource now supplies it by name with mcp_action, resolved when the catalog is built rather than when declared, so a declaration may sit either side of the include and is discarded with the config on a reload. Four things the concern shapes forced out, each of which was a real gap: Two actions of the same name — ActiveAdmin allows a member and a batch action to share one, and a concern declaring both is how that happens — derived the same tool name. tools/list advertised it twice and find returned the member one, leaving the batch action permanently unreachable. Both are now hidden until a tool_name: tells them apart. A member action declared method: [:post, :delete] is one action with two meanings, the second usually undoing the first, but only the first verb was reachable. An annotation may now pick the verb, and an action may carry several annotations, so both meanings become tools. A batch action titled with a String gets a symbol ActiveAdmin derives by mangling that title, punctuation included. Applications generate these in loops from data, so an annotation may name the title instead, and the derived tool name has the punctuation squeezed out. ActiveAdmin's own :if proc on a batch action is now honoured. ActiveAdmin consults it only when rendering, so it will dispatch an action it refuses to display; this is deliberately stricter, because MCP should not be the way round a gate the admin enforces by not offering the button. A proc form: is evaluated in controller context the way ActiveAdmin evaluates it, so those batch actions contribute their param types. That runs application code, so it is lazy: never during catalog construction, only once the caller has authorized the action, which is the posture suggestions: already has. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
🤖 Closes #16.
Why
An action opts in to MCP by carrying an
mcp:key on its own declaration. That reaches nothing declared in a shared concern or by another gem — and even where the concern can be edited, onemcp:key there would describe every resource that includes it identically, which is the opposite of what's wanted.Such actions are already registered on the resource config as ordinary
ControllerAction/BatchActionobjects, so only the metadata was missing.mcp_actionIt annotates; it never declares. Annotations live on the resource config and are resolved when the catalog is built, so a declaration may sit either side of the
include, and they are discarded with the config on a development reload. Naming an action the resource does not have warns and skips; naming one that matches more than one action withoutkind:is refused rather than guessed. An annotation replaces an inlinemcp:wholesale rather than merging into it.Four gaps the real concern shapes forced out
Each of these was already broken; the concerns are just what reached them.
Two actions of the same name. ActiveAdmin lets a
member_actionand abatch_actionshare a name, and a concern declaring both is how that happens. Both derived the same tool name, sotools/listadvertised it twice andActionCatalog.findalways returned the member one — the batch action was permanently unreachable, silently. Both are now hidden until atool_name:tells them apart.method: [:post, :delete]. One action with two meanings, the second usually undoing the first, buthttp_verbtookArray(...).firstso only the first was reachable. An annotation may now pick the verb, and an action may carry several annotations, so both meanings become tools.Batch actions titled with a String. ActiveAdmin derives the symbol by titleizing and underscoring the title, which leaves punctuation in it (
"Warning: Fridge left open"→:"warning:_fridge_left_open"). Applications generate these in loops from data, so requiring the annotation to name the mangled symbol would be unusable — it may name the title instead, and the derived tool name has the punctuation squeezed out.ActiveAdmin's
:ifproc. A batch action the admin UI hides because:ifrefuses was listed and runnable over MCP. Now neither. This is deliberately stricter than ActiveAdmin, which consults:ifonly when rendering and will happily dispatch an action it refuses to display — MCP should not be the way round a gate the admin enforces by not offering the button. A proc that raises, typically because it reads request state a listing cannot supply, hides the tool and logs why.Proc
form:Evaluated in controller context exactly as ActiveAdmin evaluates it, so a batch action whose form varies by resource contributes its param types instead of nothing.
That runs application code, so it is lazy: never during catalog construction, only once the caller has authorized the action — the posture
suggestions:already has. Threading the authenticated user throughActionCatalogandActionDefinition, which were user-agnostic, is what makes that possible.One trade-off to flag. #16 proposed recovering the "is this declared param actually among the form's keys?" check for proc forms too. That check runs during validation, at catalog-build time — the one moment the proc must not be evaluated. Rather than run application code for unauthorized users to get a declaration-error nicety, I kept the check skipped for proc forms, as it is today. Types are inherited; a mistyped param name on a proc-form batch action is still silently dropped by ActiveAdmin. Say the word if you'd rather have it the other way.
Testing
self.includeddeclaring a member action with two verbs, a batch action of the same name with a procform:, and an:if-gated batch action.form:, batch actions generated in a loop and titled with a String, an:if-gated batch action, and a controllerbefore_actionguarding the batch dispatch. Two resources include it and only one annotates, proving exposure stays per resource.:ifproc to always admit, removing thebefore_action, renaming the loop titles so the annotations no longer match, droppingkind:, and breaking the member action's body. Each targeted example failed; fixtures restored and both suites re-run green.README gains a section on annotating actions you can't edit; CHANGELOG records the addition and the three behaviour changes.
🤖 Generated with Claude Code