You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A designer or agent using the Plasmic MCP wants to wire an event on a slot child (typically an <input>onChange or <button>onClick) to invoke a refAction exposed by an ancestor code-component. For example, in the EP catalog-search package, the EPSearchBox provider exposes setValue and clear ref-actions, and the designer wants the slot's input/button to call them via Plasmic's interaction system. This is the canonical "headless component plus designer-controlled chrome" pattern.
In Plasmic Studio's UI, this is one well-known choice: the user adds an interaction, picks "Run element action" from the action dropdown, picks the target instance, picks the action method, fills in arg expressions. Internally Studio writes an Interaction of type invokeRefAction whose codegen produces ({tplRef, action, args}) => $refs?.[tplRef]?.[action]?.(...(args ?? [])). This is a first-class Plasmic concept defined in platform/wab/src/wab/shared/core/states.ts:invokeRefAction.
The MCP's interaction.add tool today only exposes three actionName values: navigation, updateVariable, customFunction. invokeRefAction is not in the enum. Agents and designers driving Plasmic via the MCP cannot wire ref-action invocations the canonical way. The customFunction workaround ((() => { $refs[name].setValue(event.target.value); })()) saves cleanly but does not execute at runtime — $refs is not in scope at the customFunction's call site for HTML element event handlers (verified live by typing into a wired input: the DOM value updates but the parent's setValue is never called and the InstantSearch query never refines).
In the headless-component flow PRD #308 just landed, the MCP successfully drove every other step end-to-end:
node.add populated the EPSearchBox slot with a Plasmic <input> and <button> (TplTags, which escape Plasmic's TPL_COMPONENT_PROPS appearance filter).
node.update-attrs set static attrs and the dynamic value binding to $ctx.searchFieldData.value.
node.update-styles applied border, padding, font, background, color, radius — every appearance property the designer set in Plasmic Studio's panel reached the live DOM, validating the architectural answer to designer-controlled headless code components.
The two final wiring steps — input onChange to invoke setValue, button onClick to invoke clear — could not be completed via MCP. The designer must drop into Studio UI and click through the "Run element action" dropdown for each one.
This breaks the headless-component design pattern at the final 5% of the flow, forcing a Studio-UI handoff for every interactive code component the MCP creates. It also means PRD #308's slot-composition design only fully ships value when used via the MCP plus Studio UI, not via the MCP alone — undermining the design's "agent can drive a complete UI build" promise.
Solution
The MCP gains the ability to wire invokeRefAction interactions on a TplTag's event handler, mirroring Studio UI's "Run element action" workflow as closely as possible and reusing the existing Plasmic-shared code paths for codegen, model construction, and validation rather than re-implementing them.
Concretely, the agent/designer can add an interaction shaped like this and have it work end-to-end without Studio UI:
interaction.add with actionName: "invokeRefAction" (or alias runAction/runElementAction).
Args:
tplRef resolved to a TplComponent instance via the same node-resolution helpers node.add/update-styles/etc. already use (UUID, name, path, or numeric index). The resolver rejects targets that are not TplComponent instances of code components with refActions registered.
action is the ref-action method name; the resolver looks up the target component's refActions map (via the same dev-host registry the MCP loads at project.set) and rejects unknown action names with a clear error listing the valid options.
args accept either an ordered array of arg-expression strings OR an object keyed by the action's argType name. Each arg becomes a FunctionArg inside a CollectionExpr, with the same argType resolution Studio uses (propTypeToWabType → typeFactory.func).
The resulting Interaction model is byte-for-byte identical to what Studio UI would emit for the same wiring — same actionName, same NameArg structure for tplRef/action/args, same TplRef and CollectionExpr and FunctionArg model classes.
Codegen for invokeRefAction is already shared (platform/wab/src/wab/shared/core/states.ts); the MCP does not write a new template. The runtime $refs?.[tplRef]?.[action]?.(...(args ?? [])) is what Plasmic emits regardless of whether the interaction was authored in Studio UI or via the MCP.
After the fix, the EP catalog-search wiring example becomes a clean MCP path:
No customFunction shim, no $refs confusion, no Studio-UI handoff.
User Stories
As an agent driving Plasmic via the MCP, I want to wire an <input>onChange to invoke a parent code-component's setValue ref-action via a single tool call, so that I can complete a headless-component build without forcing the user into Studio UI for the final step.
As an agent, I want to wire a <button>onClick to invoke a parent code-component's clear ref-action via the same invokeRefAction action type, with no special-case code path for argument-less actions versus actions that take arguments.
As an agent, I want to reference the target TplComponent by the same identifier shapes I use everywhere else in the MCP (UUID, name, path, index), so that I do not have to learn a new resolution dialect for ref-actions.
As an agent, when I supply a tplRef that resolves to a non-TplComponent element (e.g. a <div>) or a code-component with no refActions registered, I want the MCP to reject the call with a clear error listing valid candidates, so that I can correct the call before saving and avoid corrupt bundles.
As an agent, when I supply an action name that the target component does not expose, I want the MCP to reject the call with a clear error listing the available action names on that target, so that I can pick the right one.
As an agent, when I supply too few or too many args for a refAction, I want the MCP to reject the call with a clear error showing the action's expected argTypes and the count it received, so that I do not commit a partially-wired interaction.
As an agent, when I supply an args value whose type does not match the corresponding argType (e.g. passing a number where a string is expected), I want the MCP to reject the call before save with a type-mismatch error mirroring Studio's coercion rules.
As an agent, I want to supply args either as an ordered array (matching the action's argTypes order, like Studio UI's positional bindings) OR as an object keyed by argType name, so that I can choose whichever shape makes my prompt more readable.
As an agent, after wiring the interaction, I want interaction.list to return a structured representation of the invokeRefAction interaction (event, actionName, target instance ref, action name, arg expressions), so that I can verify what I wrote and round-trip-edit it.
As an agent, when I want to remove a previously-wired ref-action interaction, interaction.remove should work the same way it does for navigation/updateVariable/customFunction interactions, so that I do not need a special workflow for ref-action interactions.
As an agent, when I want to update an existing invokeRefAction interaction (e.g. change the action method or arg expressions), interaction.update should accept the same args shape as interaction.add, so that I can iterate without removing-then-adding.
As a designer using Plasmic Studio, I want interactions wired via the MCP to be visually indistinguishable from interactions I wire myself in the UI — same display name "Run element action", same parameters, same target picker — so that handoff between agent-driven and human-driven editing is seamless.
As a designer, I want existing Studio-UI-wired invokeRefAction interactions to be readable by the MCP's interaction.list after switching to MCP-driven editing, so that mixed workflows do not force a re-wiring step.
As a developer maintaining the MCP, I want the implementation to reuse Plasmic's shared invokeRefAction codegen template (platform/wab/src/wab/shared/core/states.ts) and model classes (TplRef, NameArg, CollectionExpr, FunctionArg) rather than duplicating the codegen, so that future Plasmic upstream changes flow through the MCP without divergence.
As a developer, I want the MCP's refAction resolution logic to use the same dev-host registry / code-component metadata path that Studio's getTplRefActions reads from, so that there is one source of truth for what refActions exist on a given TplComponent instance.
As a developer, I want a regression test for the model-shape parity between MCP-authored invokeRefAction interactions and Studio-authored ones, so that I can guarantee mixed workflows do not produce subtly-different bundles.
As a downstream consumer (e.g. EP catalog-search), I want the existing PRD PRD: Make EPSearchBox styleable by lifting its DOM out of the code-component #308 EPSearchBox example app's setValue / clear wiring to be completable via MCP without Studio UI involvement, so that the published agent recipes for headless components are end-to-end MCP-driven.
Implementation Decisions
The MCP's interaction.add tool gains invokeRefAction as a new actionName value. The new value is added to the existing ACTION_ALIASES map, the SUPPORTED_ACTIONS set, and resolveActionName validation in the same file/structure today's three actions live in. User-friendly aliases (runAction, runElementAction) are accepted via the same alias map.
buildActionArgs gains an invokeRefAction case that constructs the same NameArg array Studio's UI builder produces, using the existing shared model classes (TplRef, NameArg, CustomCode, CollectionExpr, FunctionArg) — no new model classes, no new codegen template, no shadow validation logic.
tplRef resolution uses the existing node-resolution helper (resolveNode) that the rest of the MCP uses, with an additional type guard: the resolved node must be a TplComponent of a registered code component whose meta has at least one entry in refActions. Error messages list the valid candidate uuids/names within the same parent component scope.
action validation mirrors Studio's getTplRefActions logic: look up the resolved TplComponent's component.codeComponentMeta.refActions (the same source Studio's studioCtx.getCodeComponentMeta returns). Server-side, this means walking the dev-host registry that's already loaded at project.set time. Unknown action names are rejected with a list of valid keys.
args shape: accept either Array<string> (ordered, position-matched against argTypes) or Record<string, string> (keyed by argType name). Internally normalise to an ordered array indexed by argTypes order, then build one FunctionArg per argType. Missing args for required argTypes are rejected; extra args are rejected; type-mismatch detection delegates to the same propTypeToWabType machinery Studio uses (or equivalent shared utility).
interaction.list reads back invokeRefAction by adding a case to the existing arg-extraction switch that walks an interaction's args array. Returns { tplRef: <name-or-uuid>, action: <string>, args: <Array<expression-string>> }. The shape is symmetric with the input format so round-trip edits work.
interaction.update automatically supports invokeRefAction once add does, since update reuses the same args-building path.
No changes to interaction.remove — it operates on interaction index/uuid and is already action-type-agnostic.
The MCP imports the invokeRefAction action descriptor from the shared wab module if available, rather than redefining its codegen template. If the shared module's exports do not currently make this clean, a small refactor in platform/wab/src/wab/shared/core/states.ts to export the metadata for external consumers is part of this PRD's scope.
Tool description update: interaction.add's description gains a invokeRefAction example. The description also adds a one-line note about customFunction's closure scope (gap build: trigger fresh terraform plans after service discovery permissions #78 already filed; can be addressed in the same change set if convenient).
Backwards compatibility: existing customFunction interactions that emulate invokeRefAction via $refs continue to be valid. We do NOT auto-migrate them; we just add the cleaner native path. A follow-up PRD could detect the $refs[...].action(...) pattern in customFunction bodies and offer migration, but that is out of scope here.
Testing Decisions
The behavioural test suite for interaction.add lives at packages/plasmic-mcp/src/__tests__/interaction.test.ts. New invokeRefAction test cases are added here, alongside the existing navigation / updateVariable / customFunction cases.
A good test for this work asserts the observable model shape that Plasmic's codegen will see — i.e. the Interaction object's actionName, args array contents (NameArg name, expr type, expr fields). It does NOT assert the runtime DOM behaviour (that requires a full integration harness; see Out of scope). It does NOT assert internal MCP intermediate state.
Specific assertions:
interaction.add with invokeRefAction, valid tplRef (UUID), valid action, ordered-array args produces an Interaction whose args contains: a NameArg tplRef wrapping a TplRef whose tpl is the resolved TplComponent; a NameArg action with a CustomCode whose code is JSON.stringify(action); a NameArg args with a CollectionExpr whose entries are FunctionArgs in argType order.
Same with args as an object — produces the identical Interaction. Asserts the two input shapes converge.
Unknown tplRef raises with a clear error mentioning node-not-found and listing nearest-matches by name.
tplRef resolves to a TplTag (not a TplComponent) — rejects with "must be a TplComponent with refActions" error.
tplRef resolves to a TplComponent but the code-component meta has no refActions — rejects with "this component does not expose refActions" listing the friendly displayName.
Unknown action — rejects with "action not found; available: ".
Wrong arg count — rejects with "expected N args (); received M".
Wrong arg type — rejects with "arg expected type ; received ".
Round-trip: interaction.add then interaction.list returns an entry with the same tplRef/action/args shape that was input.
Cross-authoring parity: an Interaction object built via the MCP's invokeRefAction path is structurally equal to the Interaction object Studio's interactions-meta.ts:invokeRefAction would build for the same inputs (sharedmodel comparison via existing cloneTreeChecker or equivalent equality helper).
Prior art: the existing interaction.test.ts uses mockApiClient, mockEnsureBaseVariantSetting, mockWithRecording patterns. The new tests follow the same patterns with no new test infra introduced.
Out of Scope
Wiring an invokeEventHandler (the cousin action type for invoking a Plasmic-defined event prop on a child component, not a refAction). That action is structurally similar but distinct — separate PRD if needed.
Auto-migrating existing customFunction interactions that emulate invokeRefAction via $refs[name].action(...) to the canonical type. Worth a follow-up once the canonical path is widely used.
Adding the invokeEventHandler / dataSourceOp / customFunctionOp / login / logout / updateVariant / invalidateDataQuery action types that Studio's interactions-meta.ts also defines but the MCP does not yet expose. Each is a separate gap.
Updating gap build: trigger fresh terraform plans after service discovery permissions #78 (interaction.add customFunction tool description omits $refs). Once invokeRefAction is the canonical path, the customFunction $refs confusion is much less load-bearing — the description fix can be handled in a doc-only PR.
A new MCP tool dedicated to ref-action wiring (e.g. interaction.add-ref-action). The existing interaction.add with a new actionName value is the right surface; no new tool needed.
Further Notes
Three decisions taken without explicit confirmation. Reviewers can override before implementation:
args shape: support both array and object. Studio's UI internally uses positional argTypes order, so the MCP's normalised internal shape is an array. Accepting an object input is a developer-experience addition; if it complicates the implementation we can drop it and keep array-only.
Hard-error on validation failures, not warn-and-save. A saved invalid invokeRefAction would corrupt the bundle (target component IID points to nothing meaningful). Hard-error matches the rest of the MCP's write-action validation pattern (per gap chore: add Getting Started guide for Plasmic development #25).
Reuse Studio's shared model construction directly, not a re-implementation. Specifically: import invokeRefAction parameters/codegen template from the wab shared module if the module exports them; if not, treat that small refactor as part of this PRD. Avoid drift by construction.
Open questions worth a comment before implementation:
Does the dev-host registry the MCP loads at project.set carry refActions in the form Studio's getTplRefActions expects? Need to verify before validation can be cheap. If not, the MCP needs an extra registry-side enrichment pass.
Does the tplRefTplRef model class require a name on the target TplComponent, or does it accept anonymous TplComponents? Studio UI's auto-naming flow may matter here (Plasmic refs use tpl.name mangled via toVarName; anonymous TplComponents have no name). If anonymous, the MCP may need to either auto-name the target or reject the call with "target instance must be named first".
Should interaction.update accept partial args (e.g. only change the action while keeping tplRef and args)? Studio's UI lets users update one parameter at a time; the MCP could mirror this by treating any unspecified arg as "leave as-is".
Problem Statement
A designer or agent using the Plasmic MCP wants to wire an event on a slot child (typically an
<input>onChangeor<button>onClick) to invoke arefActionexposed by an ancestor code-component. For example, in the EP catalog-search package, theEPSearchBoxprovider exposessetValueandclearref-actions, and the designer wants the slot's input/button to call them via Plasmic's interaction system. This is the canonical "headless component plus designer-controlled chrome" pattern.In Plasmic Studio's UI, this is one well-known choice: the user adds an interaction, picks "Run element action" from the action dropdown, picks the target instance, picks the action method, fills in arg expressions. Internally Studio writes an
Interactionof typeinvokeRefActionwhose codegen produces({tplRef, action, args}) => $refs?.[tplRef]?.[action]?.(...(args ?? [])). This is a first-class Plasmic concept defined inplatform/wab/src/wab/shared/core/states.ts:invokeRefAction.The MCP's
interaction.addtool today only exposes threeactionNamevalues:navigation,updateVariable,customFunction.invokeRefActionis not in the enum. Agents and designers driving Plasmic via the MCP cannot wire ref-action invocations the canonical way. ThecustomFunctionworkaround ((() => { $refs[name].setValue(event.target.value); })()) saves cleanly but does not execute at runtime —$refsis not in scope at the customFunction's call site for HTML element event handlers (verified live by typing into a wired input: the DOM value updates but the parent'ssetValueis never called and the InstantSearch query never refines).In the headless-component flow PRD #308 just landed, the MCP successfully drove every other step end-to-end:
node.addpopulated the EPSearchBox slot with a Plasmic<input>and<button>(TplTags, which escape Plasmic'sTPL_COMPONENT_PROPSappearance filter).node.update-attrsset static attrs and the dynamicvaluebinding to$ctx.searchFieldData.value.node.update-stylesapplied border, padding, font, background, color, radius — every appearance property the designer set in Plasmic Studio's panel reached the live DOM, validating the architectural answer to designer-controlled headless code components.onChangeto invokesetValue, buttononClickto invokeclear— could not be completed via MCP. The designer must drop into Studio UI and click through the "Run element action" dropdown for each one.This breaks the headless-component design pattern at the final 5% of the flow, forcing a Studio-UI handoff for every interactive code component the MCP creates. It also means PRD #308's slot-composition design only fully ships value when used via the MCP plus Studio UI, not via the MCP alone — undermining the design's "agent can drive a complete UI build" promise.
Solution
The MCP gains the ability to wire
invokeRefActioninteractions on a TplTag's event handler, mirroring Studio UI's "Run element action" workflow as closely as possible and reusing the existing Plasmic-shared code paths for codegen, model construction, and validation rather than re-implementing them.Concretely, the agent/designer can add an interaction shaped like this and have it work end-to-end without Studio UI:
interaction.addwithactionName: "invokeRefAction"(or aliasrunAction/runElementAction).tplRefresolved to a TplComponent instance via the same node-resolution helpersnode.add/update-styles/etc. already use (UUID, name, path, or numeric index). The resolver rejects targets that are notTplComponentinstances of code components withrefActionsregistered.actionis the ref-action method name; the resolver looks up the target component'srefActionsmap (via the same dev-host registry the MCP loads atproject.set) and rejects unknown action names with a clear error listing the valid options.argsaccept either an ordered array of arg-expression strings OR an object keyed by the action's argTypename. Each arg becomes aFunctionArginside aCollectionExpr, with the sameargTyperesolution Studio uses (propTypeToWabType→typeFactory.func).Interactionmodel is byte-for-byte identical to what Studio UI would emit for the same wiring — sameactionName, sameNameArgstructure fortplRef/action/args, sameTplRefandCollectionExprandFunctionArgmodel classes.invokeRefActionis already shared (platform/wab/src/wab/shared/core/states.ts); the MCP does not write a new template. The runtime$refs?.[tplRef]?.[action]?.(...(args ?? []))is what Plasmic emits regardless of whether the interaction was authored in Studio UI or via the MCP.After the fix, the EP catalog-search wiring example becomes a clean MCP path:
No customFunction shim, no
$refsconfusion, no Studio-UI handoff.User Stories
<input>onChangeto invoke a parent code-component'ssetValueref-action via a single tool call, so that I can complete a headless-component build without forcing the user into Studio UI for the final step.<button>onClickto invoke a parent code-component'sclearref-action via the sameinvokeRefActionaction type, with no special-case code path for argument-less actions versus actions that take arguments.tplRefthat resolves to a non-TplComponentelement (e.g. a<div>) or a code-component with norefActionsregistered, I want the MCP to reject the call with a clear error listing valid candidates, so that I can correct the call before saving and avoid corrupt bundles.actionname that the target component does not expose, I want the MCP to reject the call with a clear error listing the available action names on that target, so that I can pick the right one.argsfor a refAction, I want the MCP to reject the call with a clear error showing the action's expectedargTypesand the count it received, so that I do not commit a partially-wired interaction.argsvalue whose type does not match the correspondingargType(e.g. passing a number where a string is expected), I want the MCP to reject the call before save with a type-mismatch error mirroring Studio's coercion rules.argseither as an ordered array (matching the action'sargTypesorder, like Studio UI's positional bindings) OR as an object keyed by argTypename, so that I can choose whichever shape makes my prompt more readable.interaction.listto return a structured representation of theinvokeRefActioninteraction (event, actionName, target instance ref, action name, arg expressions), so that I can verify what I wrote and round-trip-edit it.interaction.removeshould work the same way it does for navigation/updateVariable/customFunction interactions, so that I do not need a special workflow for ref-action interactions.invokeRefActioninteraction (e.g. change the action method or arg expressions),interaction.updateshould accept the same args shape asinteraction.add, so that I can iterate without removing-then-adding.invokeRefActioninteractions to be readable by the MCP'sinteraction.listafter switching to MCP-driven editing, so that mixed workflows do not force a re-wiring step.invokeRefActioncodegen template (platform/wab/src/wab/shared/core/states.ts) and model classes (TplRef, NameArg, CollectionExpr, FunctionArg) rather than duplicating the codegen, so that future Plasmic upstream changes flow through the MCP without divergence.getTplRefActionsreads from, so that there is one source of truth for what refActions exist on a given TplComponent instance.invokeRefActioninteractions and Studio-authored ones, so that I can guarantee mixed workflows do not produce subtly-different bundles.setValue/clearwiring to be completable via MCP without Studio UI involvement, so that the published agent recipes for headless components are end-to-end MCP-driven.Implementation Decisions
interaction.addtool gainsinvokeRefActionas a newactionNamevalue. The new value is added to the existingACTION_ALIASESmap, theSUPPORTED_ACTIONSset, andresolveActionNamevalidation in the same file/structure today's three actions live in. User-friendly aliases (runAction,runElementAction) are accepted via the same alias map.buildActionArgsgains aninvokeRefActioncase that constructs the same NameArg array Studio's UI builder produces, using the existing shared model classes (TplRef, NameArg, CustomCode, CollectionExpr, FunctionArg) — no new model classes, no new codegen template, no shadow validation logic.tplRefresolution uses the existing node-resolution helper (resolveNode) that the rest of the MCP uses, with an additional type guard: the resolved node must be aTplComponentof a registered code component whose meta has at least one entry inrefActions. Error messages list the valid candidate uuids/names within the same parent component scope.actionvalidation mirrors Studio'sgetTplRefActionslogic: look up the resolved TplComponent'scomponent.codeComponentMeta.refActions(the same source Studio'sstudioCtx.getCodeComponentMetareturns). Server-side, this means walking the dev-host registry that's already loaded atproject.settime. Unknown action names are rejected with a list of valid keys.argsshape: accept eitherArray<string>(ordered, position-matched againstargTypes) orRecord<string, string>(keyed by argType name). Internally normalise to an ordered array indexed byargTypesorder, then build oneFunctionArgper argType. Missing args for required argTypes are rejected; extra args are rejected; type-mismatch detection delegates to the samepropTypeToWabTypemachinery Studio uses (or equivalent shared utility).interaction.listreads backinvokeRefActionby adding a case to the existing arg-extraction switch that walks an interaction'sargsarray. Returns{ tplRef: <name-or-uuid>, action: <string>, args: <Array<expression-string>> }. The shape is symmetric with the input format so round-trip edits work.interaction.updateautomatically supportsinvokeRefActiononceadddoes, since update reuses the same args-building path.invokeRefActionaction descriptor from the shared wab module if available, rather than redefining its codegen template. If the shared module's exports do not currently make this clean, a small refactor inplatform/wab/src/wab/shared/core/states.tsto export the metadata for external consumers is part of this PRD's scope.interaction.add's description gains ainvokeRefActionexample. The description also adds a one-line note aboutcustomFunction's closure scope (gap build: trigger fresh terraform plans after service discovery permissions #78 already filed; can be addressed in the same change set if convenient).$refscontinue to be valid. We do NOT auto-migrate them; we just add the cleaner native path. A follow-up PRD could detect the$refs[...].action(...)pattern in customFunction bodies and offer migration, but that is out of scope here.Testing Decisions
interaction.addlives atpackages/plasmic-mcp/src/__tests__/interaction.test.ts. NewinvokeRefActiontest cases are added here, alongside the existing navigation / updateVariable / customFunction cases.Interactionobject'sactionName,argsarray contents (NameArg name, expr type, expr fields). It does NOT assert the runtime DOM behaviour (that requires a full integration harness; seeOut of scope). It does NOT assert internal MCP intermediate state.interaction.addwithinvokeRefAction, validtplRef(UUID), validaction, ordered-arrayargsproduces an Interaction whoseargscontains: a NameArgtplRefwrapping aTplRefwhosetplis the resolved TplComponent; a NameArgactionwith aCustomCodewhose code isJSON.stringify(action); a NameArgargswith aCollectionExprwhose entries areFunctionArgs in argType order.argsas an object — produces the identical Interaction. Asserts the two input shapes converge.tplRefraises with a clear error mentioningnode-not-foundand listing nearest-matches by name.tplRefresolves to aTplTag(not a TplComponent) — rejects with "must be a TplComponent with refActions" error.tplRefresolves to a TplComponent but the code-component meta has norefActions— rejects with "this component does not expose refActions" listing the friendly displayName.action— rejects with "action not found; available: ".interaction.addtheninteraction.listreturns an entry with the sametplRef/action/argsshape that was input.invokeRefActionpath is structurallyequalto the Interaction object Studio'sinteractions-meta.ts:invokeRefActionwould build for the same inputs (sharedmodel comparison via existingcloneTreeCheckeror equivalent equality helper).interaction.test.tsusesmockApiClient,mockEnsureBaseVariantSetting,mockWithRecordingpatterns. The new tests follow the same patterns with no new test infra introduced.Out of Scope
invokeEventHandler(the cousin action type for invoking a Plasmic-defined event prop on a child component, not a refAction). That action is structurally similar but distinct — separate PRD if needed.setValueat runtime when typed into. Browser-level verification happens manually post-merge via Playwright MCP, mirroring the verification flow for PR feat(ep-commerce): headless styling contract + EPSearchBox provider refactor (#305, #308) #309.customFunctioninteractions that emulateinvokeRefActionvia$refs[name].action(...)to the canonical type. Worth a follow-up once the canonical path is widely used.invokeEventHandler/dataSourceOp/customFunctionOp/login/logout/updateVariant/invalidateDataQueryaction types that Studio'sinteractions-meta.tsalso defines but the MCP does not yet expose. Each is a separate gap.interaction.addcustomFunction tool description omits$refs). OnceinvokeRefActionis the canonical path, the customFunction$refsconfusion is much less load-bearing — the description fix can be handled in a doc-only PR.interaction.add-ref-action). The existinginteraction.addwith a newactionNamevalue is the right surface; no new tool needed.Further Notes
Three decisions taken without explicit confirmation. Reviewers can override before implementation:
argsshape: support both array and object. Studio's UI internally uses positional argTypes order, so the MCP's normalised internal shape is an array. Accepting an object input is a developer-experience addition; if it complicates the implementation we can drop it and keep array-only.invokeRefActionparameters/codegen template from the wab shared module if the module exports them; if not, treat that small refactor as part of this PRD. Avoid drift by construction.Open questions worth a comment before implementation:
project.setcarryrefActionsin the form Studio'sgetTplRefActionsexpects? Need to verify before validation can be cheap. If not, the MCP needs an extra registry-side enrichment pass.tplRefTplRefmodel class require anameon the target TplComponent, or does it accept anonymous TplComponents? Studio UI's auto-naming flow may matter here (Plasmic refs usetpl.namemangled viatoVarName; anonymous TplComponents have noname). If anonymous, the MCP may need to either auto-name the target or reject the call with "target instance must be named first".interaction.updateaccept partial args (e.g. only change theactionwhile keepingtplRefandargs)? Studio's UI lets users update one parameter at a time; the MCP could mirror this by treating any unspecified arg as "leave as-is".References
platform/wab/src/wab/shared/core/states.ts:invokeRefAction.function.platform/wab/src/wab/client/state-management/interactions-meta.ts:ACTIONS_META.invokeRefAction.platform/wab/src/wab/client/state-management/ref-actions.tsx:getTplRefActions.packages/plasmic-mcp/src/edit-tools.ts—ACTION_ALIASES,SUPPORTED_ACTIONS,resolveActionName,buildActionArgs.packages/plasmic-mcp/src/__tests__/interaction.test.ts.