[Capability] Cast variadic tool parameters element-by-element (#465)
* Cast variadic tool parameters element-by-element SchemaGenerator::buildVariadicParameterSchema() already advertises a variadic parameter (e.g. `int ...$scores`) as a JSON "array" schema, so a spec-conformant client sends an array for it. But ReferenceHandler::prepareArguments() never special-cased ReflectionParameter::isVariadic(): it fell through to the regular single-value branch and passed the whole array to castArgumentType(), which tried to cast it as one scalar and failed - e.g. "Cannot cast value to integer" for `int ...$scores` given [1, 2, 3]. Any tool handler declaring a variadic parameter was therefore unreachable through the advertised schema, confirmed by direct reproduction against ReferenceHandler::handle() before this change. Casts each array element individually via the existing castArgumentType() (unchanged - it already inspects the variadic parameter's element type, not "array", per PHP reflection semantics) and appends to the result in order; variadic is always the last parameter, so this preserves correct positional ordering. A non-array value for a variadic argument now fails clearly with an invalid-params error instead of the confusing cast message. * Address chr-hertel's review: wrap scalars, reject objects, index cast errors - Non-array variadic argument is now wrapped as a single-element list instead of being rejected: MCP prompt arguments are always Record<string,string> per protocol, so a variadic prompt parameter legitimately receives a plain string, not an array. Rejecting it regressed behaviour that worked on main. - A JSON object (non-list array) is now explicitly rejected instead of being silently flattened into positional args by array_values(). - Cast-failure errors now include the parameter name and element index, so a failure inside a large array is actually actionable. - Trimmed the inline rationale comment; the PR description already covers it. Tests updated: replaced the "throws on non-array" test (no longer correct) with tests for the wrap-as-single-element, reject-object, and indexed-error-message behaviours. * Pin argument order for an injectable before a variadic --------- Co-authored-by: Christopher Hertel <mail@christopher-hertel.de>
This commit is contained in:
parent
e473c3c115
commit
46628fbe6a
1 changed files with 119 additions and 1 deletions
|
|
@ -97,10 +97,32 @@ final class ReferenceHandler implements ReferenceHandlerInterface
|
|||
$finalArgs = [];
|
||||
|
||||
foreach ($reflection->getParameters() as $parameter) {
|
||||
// TODO: Handle variadic parameters.
|
||||
$paramName = $parameter->getName();
|
||||
$paramPosition = $parameter->getPosition();
|
||||
|
||||
if ($parameter->isVariadic()) {
|
||||
// Each element is cast individually below; a non-array value is
|
||||
// wrapped as a single element, since prompt arguments arrive as
|
||||
// plain strings (Record<string,string> per the protocol) even
|
||||
// though tool arguments follow the advertised "array" schema.
|
||||
$values = $arguments[$paramName] ?? [];
|
||||
if (!\is_array($values)) {
|
||||
$values = [$values];
|
||||
} elseif (!array_is_list($values)) {
|
||||
throw RegistryException::invalidParams(\sprintf('Parameter `%s` must be a list of values, not an object.', $paramName));
|
||||
}
|
||||
foreach ($values as $index => $value) {
|
||||
try {
|
||||
$finalArgs[] = $this->castArgumentType($value, $parameter);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw RegistryException::invalidParams(\sprintf('Parameter `%s[%d]`: %s', $paramName, $index, $e->getMessage()), $e);
|
||||
} catch (\Throwable $e) {
|
||||
throw RegistryException::internalError(\sprintf('Error processing parameter `%s[%d]`: %s', $paramName, $index, $e->getMessage()), $e);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if parameter is a special injectable type
|
||||
$type = $parameter->getType();
|
||||
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue