Install
Works with Drupal: ^10.5 || ^11Using Composer to manage Drupal site dependencies
Alternative installation files
Download tar.gz
653.95 KB
MD5: b7bfe68a868618458204ca6ca137479a
SHA-1: 92ce55aa1473d087fc81697e4240769684232933
SHA-256: 6448bc5d2bb05d885c052525154bee0bac0ba27b2152f810cbdcece250c4602c
Download zip
917.81 KB
MD5: 3e2f084f7c4fdd07ca3fec658986a603
SHA-1: cb3fa3b0aab7d3ee59c4c09cfd8b68cc04f7cd48
SHA-256: c4bfffb8b86f6e7f2a261c645f3f65006ea6114943fa3ac54cf09080583bd978
Release notes
Beta12 is about callers that can correct themselves. Every tool call runs through one sequence. Every failure names the input, item or property it is about. The advertised schema says what a tool really accepts and returns. And a service method can now become a tool with a few attributes, with no plugin class.
Tools from typed methods
- Put
#[ToolMethod]on a public service method and it is a tool: parameters are inputs, the return value is the output, the docblock supplies the descriptions (#3518120). Scalars, entities, backed enums,DateTimeInterface, docblock list and shape types and value objects are read from the signature;#[ToolInput],#[ToolEntityInput],#[ToolOutput]and#[ToolProperty]say what PHP cannot, andconfig: 'system.site'builds a definition from config schema. - Access is never inferred. A method declares a
permission, an entity operation with#[ToolEntityInput(op: 'update')], or an access method on the same service; a method with no check fails discovery, as does anything the describer cannot type. - A plugin can implement a typed
__invoke()instead ofdoExecute(). The guide isdocs/developers/tool-methods.md.
One sequence for every call
- The new
tool.runnerservice (and itsToolManager::run()facade) owns the invoker sequence: declared permission before the tool is instantiated, inputs, validation,access(), thenexecute()(#3583078). An explicitaccount:runs the call as that user. It returns aToolInvocationcarrying the result, the formatted result and the resolved input values, even on a failed call. - Drush
tool:runand the AI connector now run through it. The MCP bridge adopts it in mcp_server_tool_bridge!29. ToolMessagesholds the shared plain-text formatters, so every invoker prints one denial shape (Access denied for tool <id>. <reason>) and one invalid-input shape (#3583082).- A denial reason reaches the caller as the tool wrote it. The message escapes it once, and text that looks like a tag or an entity is no longer stripped or decoded (#3583109).
Failures say what failed
- A violation's message is now the reason alone. The input's name is its property path, and item and property positions join it (
codes.0,product.sku,rows.0.sku) (#3583085, #3583065). Output violations follow the same shape. - Failed results carry their violations:
getViolations()on the result, andInputExceptioncarries them too (getViolations(),getInputNames(), a plain-textgetMessage()). A tool's own cross-input check throwsnew InputException($reason, inputNames: ['end_date']). - Every input that cannot be stored is reported in the same response, named on its path: a malformed or unknown entity handle, a locked or undeclared input. The inputs that were stored are still validated, so a bad handle and a bad value arrive together (#3583091):
Invalid input: entity: The handle "handle:nope" is not a valid handle. mode: The value you selected is not a valid choice. - The "This input must be an entity." violation is the reason alone for every invoker; it no longer guesses whether the invoker can pass entities.
drush tool:runadds its own advice for an entity input it cannot fill (#3583083). execute()with a required input set to NULL is now an input failure naming the input (#3583094).- An entity's label appears in a handle hint or a Drush
tool:runentity reference only when the account passes the entity'sview labelcheck and, where the label comes from a field, field access on it (#3583108). Drush checks the--uidaccount the tool ran as. Hints now include a user's name, which core shows to every account.
The schema tells the truth
destructivecan be left undeclared, and then follows the operation: a write or trigger tool counts as destructive unless it declaresdestructive: FALSE. This reachesisDestructive(), the Drush listings and the MCP bridge'sdestructiveHint(#3583086).- Output definitions gain a
nullableflag, separate fromrequired, defaulting toFALSE: null means the output is absent unless the output says null is a value it carries (#3583043).requiredyes,nullableno: always present, never null. Schema:T, listed inrequired.requiredyes,nullableyes: always present, null when there is no value. Schema:[T, "null"], listed inrequired.requiredno,nullableno: a null is left out of the result. Schema:T, key may be missing.requiredno,nullableyes: sent asnullwhen there is no value. Schema:[T, "null"].
- Timestamp inputs advertise the integer seconds they already accepted, and a datetime input without a format marker offers both
date-timeanddate, so strict gates such as the MCP SDK no longer turn either spelling away (#3583005). Output schemas are unchanged. - A description is no longer prefixed with the label, which already goes out as
title; a list's description sits on the array instead of its items; a definition without a description sends none (#3583032). - A string can say what it contains: a
MediaTypeconstraint publishescontentMediaType(#3583076). - Declaring the generic
entitytype on an entity definition class now fails where it is written, instead of later as a missingentity:entityplugin (#3583063).
Tool Explorer
- The execute form reports the outcome once: a denial with its reason, a failure as one error, a success as one status. A constraint violation lands on the field that carries it (#3582968).
Drupal 10
- The Drupal 10 jobs now run on every pipeline, and the suite passes on PHPUnit 9 (#3583074).
- Known difference: on Drupal 10, validating a map input also validates the fields of an entity inside it and reports a map property's violation twice. Four tests skip on Drupal 10 until #3583093 settles it.
Deprecations
ContextDefinitionNormalizeris nowInputOutputDefinitionNormalizer, and the servicetool.normalizer.context_definitionis nowtool.normalizer.input_output_definition. The old names keep working with a deprecation and go in rc1 (#3583075).
Upgrading from beta11
Run database updates (drush updb); the post-update rebuilds the container for ToolManager's new arguments. Then watch for:
- Message text. Violation messages no longer start with
Input <name>:and no longer contain(item N)or(property x), and output failures readpath: reason. Code that parsed those strings readsgetPropertyPath()or usesToolMessages. InputException.getMessage()is plain text, so stop decoding it; read the input fromgetInputNames(). A thrower that cast aTranslatableMarkupto string should pass the object.- Destructive. An undeclared write or trigger tool is now reported destructive. Declare
destructive: FALSEon write tools that only add data. - Nullable outputs. An optional output that never declared
nullableno longer advertisesnull, and a NULL it returns is left out of the result. Declarenullable: TRUEto keep sending null. A nullable member or return of a method tool is published as required and nullable. - Schema text. Every description loses its label prefix. This is visible to LLM clients, which mostly read
description. - Entity definitions.
new EntityInputDefinition('entity')throws; a generic entity input isnew InputDefinition('entity'). - Plugins.
ToolBase::doExecute()is no longer abstract and defaults to__invoke(); a plugin with neither is left out at discovery.typedMethodSignature(),invokeTyped()andentityInputAccess()are now protected methods onToolBase, so a plugin method with one of those names must be renamed. - Exceptions in method tools. A method tool reports its own
\InvalidArgumentExceptionas a failed run; throwInputExceptionfor messages meant for the caller. - Entity input violations. Code that matched "cannot pass entities" in a violation message reads the input from
getPropertyPath()and the data type from the definition instead; onlydrush tool:runstill prints that advice. - Entity labels. A handle hint or Drush entity reference carries no label for an entity the account may not
view label, or whose label field it may not view.EntityHandleTransformer::getAccessibleLabel()takes an optional account. - MCP bridge. Update mcp_server_tool_bridge to its first release that requires beta12. Earlier bridge releases send input violations to MCP clients without the input's name.
- Implementors.
ToolResultInterfacegainedgetViolations()andToolInterface::setFailureResult()a third parameter. Nothing in-tree or in the sibling projects implements them directly.
Contributors
Code from @dries, @jurgenhaas, @mglaman, @michaellander and @sjpagan; review and real-module testing from @f0ns and @svendecabooter.