#connect()
Registers this instance's tools and records it for the executor.
The agent JavaScript surface: the WebMCP registrar and form companion, the AG-UI client-tool bridge, the A2UI surface checks, and the stream actions - generated from the source JSDoc and the controllers manifest.
Generated from the source JSDoc plus the introspected controllers manifest. What a component registers is on its gallery page and the WebMCP guide - this page documents the JS itself.
controller · app/javascript/poetry/agent/webmcp_controller.js
The registrar: one controller on an opted-in component root (webmcp: "country" on the helper call renders it beside the component's own controllers) registers that instance's declared tools with document.modelContext on connect and aborts them on disconnect. Components gain zero runtime code - each tool dispatches to the component's OWN controller action (the executes descriptor the Ruby contract validated at class load), passing the tool's parameters positionally in declared order.
Correctness rules the spec makes load-bearing:
documents an unregister/quick-re-register race where in-flight args for the old tool can hit the new tool's schema).
context; overlap confuses tool choice).
are still open spec issues; a string lets the agent self-correct): a missing or unknown parameter, a value of the wrong type or outside the enum, a missing action, a throwing action.
(the contract's actions return their resulting state, so an answer says what happened rather than "done"); the done marker covers actions that return nothing.
execute callback's {signal} is not forwarded (the actions are synchronous UI operations).
Values:
name (String): The instance name the tools register under (poetry.<name>.<tool>).tools (Array): The tool definitions this instance registers, as the component declared them.budget (Number, default: 20): The most tools one page registers; past it, further registrations are dropped with a console warning.Events: poetry:webmcp:executed, poetry:webmcp:registered, poetry:webmcp:unregistered
Registers this instance's tools and records it for the executor.
Forgets the instance and aborts its registrations.
Re-registers under the new name once connected.
Re-registers the new tool list once connected.
Registers every declared tool with the browser under the instance name, within the page budget; skipped when unsupported or in a preview.
Runs one of this instance's tools by its registered or short name; an unknown name resolves to an error message.
name
(string)
— the registered or short tool name
args
(Object)
— the tool's arguments
Returns (Promise<*>) — the tool's result, or an error message for an unknown name
Aborts every registration of this instance and announces it.
controller · app/javascript/poetry/agent/webmcp_form_controller.js
The declarative-form companion: a form declared with poetry_webmcp_form (toolname/tooldescription on the <form>) is registered by the BROWSER; this controller only answers an agent-invoked submit with the outcome. Chrome's SubmitEvent carries agentInvoked + respondWith(promise): we submit the form ourselves (fetch, same method/action, Turbo-Stream accepting) and respond with a short descriptive result instead of navigating, so the agent learns whether the submission succeeded and what the server said (validation errors included - it can self-correct).
The person's page then catches up with the answer (a beat after the result is handed to the browser, so a navigation can never swallow it): a GET answer is the page at that URL - a Turbo visit, or a plain navigation without Turbo; a Turbo-Stream answer renders; a redirected POST (redirect-after-create) visits where the redirect went. An HTML re-render of a failed POST stays put - the agent already holds the errors, and the person keeps their filled form.
Submits without agentInvoked (a person pressed Submit) pass through untouched: the human path stays the human path.
Events: poetry:webmcp:form-submitted
Listens for the form's submit.
Stops listening for the form's submit.
controller · app/javascript/poetry/agent/a2ui_surface_controller.js
The client side of an A2UI surface's checks: the server renders the program (every checked component's rules with absolute bindings, the bound inputs by path with their kinds, and the data model) and this controller evaluates it as the user types - a button whose own checks fail is disabled, a failing input is marked invalid and its error slot carries the message. The five validators and the three combinators are the checks vocabulary; anything else passes here and is judged by the server, which re-runs every rule on the action.
Values:
program (Object): The check program the server compiled: the checks, the bound inputs by path, and the data model.Evaluates every check once at mount.
Runs every check in the program and applies its failures.
A rule's failure message, or null when it passes or cannot be decided here.
rule
(Object)
— the check rule with its condition and message
Returns (string|null) — the failure message, or null
Reflects a check's failures on the elements bound to a key: a button disables, an input turns invalid.
key
(string)
— the bound key the elements carry
kind
(string)
— the control kind: button or input
failures
(string[])
— the failure messages, empty when the check passes
A value with its bindings read and its calls run, recursively.
value
(*)
— a literal, a binding, a call, or an array of them
Returns (*) — the resolved value
Runs a catalog function by name with its arguments resolved; an unknown name resolves to null.
name
(string)
— the catalog function
rawArgs
(Object)
— the arguments, bindings and calls unresolved
Returns (*) — the function's result, or null for an unknown name
The current value of a bound path from the form's inputs, falling back to the data model.
path
(string)
— the bound pointer
Returns (*) — the input's current value, or the model's
controller · app/javascript/poetry/agent/agui_client_tool_controller.js
The AG-UI client-tool bridge: the relay appends one of these (hidden) per tool call the agent made to a FRONTEND tool - a component tool the page declared - and this controller executes it through the registrar (the same dispatch a WebMCP call takes, so it works in every browser, modelContext or not), then POSTs the result to the continue URL. The server folds the tool message into the transcript and answers with the next run's streams, which Turbo renders. One element, one execution: the done flag makes a Turbo re-render inert.
Values:
call (Object): The pending tool call: its id, name and arguments.url (String): Where the tool result is posted.done (Boolean): Whether the call has already been answered (a restored snapshot must not answer twice).Events: poetry:agui:client-tool-executed
Runs the pending client tool once, posts its result, and marks the call done.
module · app/javascript/poetry/agent/index.js
@poetry/agent - the WebMCP runtime. Register beside poetry's own controllers:
import { Application } from "@hotwired/stimulus" import { registerPoetryControllers } from "@poetry/controllers" import { registerPoetryAgent } from "@poetry/agent" const application = Application.start() registerPoetryControllers(application) registerPoetryAgent(application)
| Constant | Description |
|---|---|
| controllers | identifier -> controller class (the manifest introspects this). |
| registerPoetryAgent | Registers the runtime's controllers, installs the versioned replace stream action the AG-UI relay and the A2UI streams emit (when Turbo is present), and the morph guard that keeps an A2UI surface's local state. |
module · app/javascript/poetry/agent/adapter.js
The document.modelContext adapter - the ONE file that knows the WebMCP surface's shape, so API churn (the spec has changed twice this summer, and the shipping browser trails it) stays a one-file fix. Everything else in the runtime talks to this.
Spec surface (rev 41d12f0): document.modelContext with registerTool(tool, {signal, exposedTo}), getTools({fromOrigins}) resolving RegisteredTool dictionaries whose inputSchema is an object, executeTool(tool, inputObject, {signal}) resolving the stringified result, and the toolchange event.
Shipping-browser deltas this file absorbs (Chrome 151, measured 2026-09-01; spec issue #278 tracks them): getTools() reports inputSchema as a serialized JSON string, and executeTool() parses ONLY a JSON-string argument - an object rejects with UnknownError("Failed to parse input arguments"), not a TypeError.
| Constant | Description |
|---|---|
| modelContext | The live ModelContext, or null where the browser exposes none - callers treat null as "do nothing", exactly like an edge bridge would. |
| supported | Whether this browser exposes a ModelContext. |
| registerTool | Registers one tool; resolves when the browser accepted it, rejects on a duplicate name, an empty name/description, or an invalid schema. |
| getTools | The registered tools with inputSchema normalized to an object: parsed when the browser serialized it, null when the text is not JSON. |
| executeTool | Executes a tool with the spec's object arguments, falling back to the JSON-string form the current Chrome build parses; when both shapes reject, the first rejection surfaces. |
| TOOL_NAME | WebMCP tool-name grammar: 1-128 chars of ASCII alphanumerics, "_", "-", ".". |
| validToolName | Whether a name fits the WebMCP tool-name grammar. |
| _resetArgumentShape | Test seam: forget the argument shape the last browser taught us. |
module · app/javascript/poetry/agent/stream_actions.js
Local state a morph must not reset inside an A2UI surface: the selection a tab strip holds, a dialog's open state, a popup's expanded state, and a control the user has edited (its value or checked state differs from what the server last rendered). The server does not know this state, so its frame would carry the defaults; canceling Turbo's before-morph-attribute event keeps the page's own.
| Constant | Description |
|---|---|
| installVersionedReplace | The versioned replace Turbo Stream action the AG-UI relay and the A2UI surface streams emit: a streamed frame re-renders the SAME element from a server stream, which inherits an out-of-order delivery race, so every payload carries data-version and this action applies only strictly-newer frames - older or duplicate frames are dropped silently. With method="morph" the newer frame morphs the element through Turbo's own replace action (idiomorph), so local state survives an update. Installed on Turbo by registerPoetryAgent when the host has Turbo and no vreplace of its own. |
| preservesLocalState | Whether a morph must leave an attribute alone to keep an A2UI surface's local state: an edited input's value or checked state, an open disclosure, or a slot-specific attribute the runtime owns. |
| installMorphStateGuard | Installs the before-morph-attribute guard once per document, so a streamed re-render morphs without clobbering local state. |