For developers¶
The Tool module is a plugin type. You extend it by writing tool plugins in your own module, and you integrate with it by discovering and running those plugins through the tool manager.
Extension points¶
- Tool plugins. Define a tool by adding a class under
src/Plugin/tool/Tool/with the#[Tool]attribute, extendingToolBase. See Creating a tool. - Service methods. Expose a public method of an existing service as a tool by
adding
#[ToolMethod]to it. Inputs, outputs, and descriptions come from the signature and docblock. See Exposing service methods as tools. - Typed data adapters. The type system that maps input and output definitions to
form elements and config schema is itself a plugin type,
Plugin/tool/TypedData/Adapter/. Add an adapter to support a new data type. See Input and output definitions. - Events. Subscribe to input and output transform events to change values based on the invoker, and to normalize events to change how definitions are serialized. See Events.
Core services¶
| Service | Purpose |
|---|---|
plugin.manager.tool |
Discovers, instantiates, and runs tool plugins. |
plugin.manager.tool.typed_data_adapter |
Maps data definitions to adapters. |
tool.handle_store |
Stores and resolves opaque handles for values. |
tool.entity_handle_transformer |
Swaps entities for handles and back. |
tool.definition_serializer |
Serializes tool definitions. |
The tool lifecycle¶
- A caller asks
plugin.manager.toolfor a tool instance, optionally passing an invoker identifier. - The caller sets input values. Each
setInputValue()dispatches aToolInputTransformEvent, so subscribers can transform the value for the invoker before it is stored. - The caller calls
execute(). The tool validates inputs, runs itsdoExecute()logic, and captures anExecutableResult. - The caller reads the result.
getResult()returns the raw result;getFormattedResult()applies per output transforms and returns aFormattedExecutableResultready for the invoker.
The invoker is the key idea: the same tool produces raw values for a deterministic caller and transformed, handle backed values for an AI or remote caller, without the tool's own code branching on the caller.
Invoking a tool¶
The tool.runner service performs the whole sequence below in one call, and
is the recommended way to run a tool:
$invocation = \Drupal::service('tool.runner')
->run('my_tool', values: ['name' => 'value']);
$result = $invocation->getResult();
An explicit account: argument means "run as": the runner switches the
current user for the duration, so access, per-result filtering, and
attribution all see one identity. The returned invocation carries the
result, the formatted result (message, hints, wire-shaped values), and the
input values as the transform pipeline resolved them.
An invoker that drives the stages itself - such as the Explorer's execute form, which builds widgets and collects values interactively - follows the same sequence by hand:
- Check the declared permission with
ToolManager::checkPermission($definition, $account)before processing any caller-supplied value. The check reads only the definition, so it needs no plugin instance - a caller that has not instantiated the tool yet checks$tool_manager->getDefinition($id)before doing so. Report a denial the way anaccess()denial is reported. - Set the caller's inputs with
setInputValue(). - Validate with
validateInputs()and report the violations, so an input problem is never reported as an access problem. The Explorer's execute form does not yet take this step - its widgets constrain the input shape, and a value they cannot constrain fails closed insideaccess()- so of the in-tree invokers,tool:runand the AI connector model the full sequence. - Check access with
access(), which re-checks the permission and then asks the tool's owncheckAccess()with the resolved values. - Execute with
execute()and read the result.
The order also bounds what each stage may reveal. Before the permission check
passes, the caller's values must not be processed at all: no transform events, no
violation messages composed from them, and no schema echo. After it, shape
feedback is the intended courtesy - violations against the advertised schema and
the correctable-failure schema echo - since the schema is already advertised at
that gate. A values-dependent checkAccess() can only deny after the values were
validated, so keeping existence-flavored detail out of constraint messages is the
tool author's responsibility; see Creating a tool.
Skipping step 1 loses the early denial, not the gate: access() still checks the
declared permission first. What it protects is everything input processing does
for an account that was never allowed to invoke the tool - entity loading from
handle resolution, refiners querying storage, violation text built from the
values.