diff --git a/src/Capability/Registry/ReferenceHandler.php b/src/Capability/Registry/ReferenceHandler.php index 99e58442..0991920c 100644 --- a/src/Capability/Registry/ReferenceHandler.php +++ b/src/Capability/Registry/ReferenceHandler.php @@ -99,10 +99,33 @@ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array $finalArgs = []; foreach ($reflection->getParameters() as $parameter) { - // TODO: Handle variadic parameters. $paramName = $parameter->getName(); $paramPosition = $parameter->getPosition(); + if ($parameter->isVariadic()) { + // SchemaGenerator advertises variadic parameters as a JSON "array" + // schema (see buildVariadicParameterSchema()), so the incoming value + // here is an array whose elements each need casting to the variadic's + // element type. Falling through to castArgumentType() below would try + // to cast the whole array as a single scalar and fail (e.g. "Cannot + // cast value to integer" for `int ...$extra`). Variadic is always the + // last parameter, so appending here preserves correct final ordering. + $values = $arguments[$paramName] ?? []; + if (!\is_array($values)) { + throw RegistryException::invalidParams(\sprintf('Parameter `%s` must be an array of values.', $paramName)); + } + foreach (array_values($values) as $value) { + try { + $finalArgs[] = $this->castArgumentType($value, $parameter); + } catch (InvalidArgumentException $e) { + throw RegistryException::invalidParams($e->getMessage(), $e); + } catch (\Throwable $e) { + throw RegistryException::internalError("Error processing parameter `{$paramName}`: {$e->getMessage()}", $e); + } + } + continue; + } + // Check if parameter is a special injectable type $type = $parameter->getType(); if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { diff --git a/tests/Unit/Capability/Registry/ReferenceHandlerTest.php b/tests/Unit/Capability/Registry/ReferenceHandlerTest.php index dadca9f5..3ac5e036 100644 --- a/tests/Unit/Capability/Registry/ReferenceHandlerTest.php +++ b/tests/Unit/Capability/Registry/ReferenceHandlerTest.php @@ -14,6 +14,7 @@ use Mcp\Capability\Registry\ElementReference; use Mcp\Capability\Registry\ReferenceHandler; use Mcp\Exception\InvalidArgumentException; +use Mcp\Exception\RegistryException; use Mcp\Server\ClientGateway; use Mcp\Server\Handler\ResourceHandlerInterface; use Mcp\Server\Handler\ToolHandlerInterface; @@ -141,4 +142,49 @@ public function testHandleThrowsForStringHandlerThatIsNeitherFunctionNorClass(): (new ReferenceHandler())->handle($reference, ['_session' => $session]); } + + public function testHandleCastsEachElementOfAnArrayArgumentForAVariadicParameter(): void + { + // SchemaGenerator advertises variadic parameters as a JSON "array" schema, + // so the array arrives here as a single named argument (not spread across + // multiple keys) and must be cast element-by-element to the variadic's type. + $closure = static fn (string $name, int ...$scores): string => \sprintf('%s:%d', $name, array_sum($scores)); + $reference = new ElementReference($closure); + + $result = (new ReferenceHandler())->handle($reference, [ + '_session' => $this->createMock(SessionInterface::class), + 'name' => 'total', + 'scores' => ['1', '2', '3'], + ]); + + $this->assertSame('total:6', $result); + } + + public function testHandleTreatsOmittedVariadicArgumentAsZeroElements(): void + { + $closure = static fn (string $name, int ...$scores): int => \count($scores); + $reference = new ElementReference($closure); + + $result = (new ReferenceHandler())->handle($reference, [ + '_session' => $this->createMock(SessionInterface::class), + 'name' => 'empty', + ]); + + $this->assertSame(0, $result); + } + + public function testHandleThrowsRegistryExceptionWhenVariadicArgumentIsNotAnArray(): void + { + $closure = static fn (string $name, int ...$scores): int => \count($scores); + $reference = new ElementReference($closure); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Parameter `scores` must be an array of values.'); + + (new ReferenceHandler())->handle($reference, [ + '_session' => $this->createMock(SessionInterface::class), + 'name' => 'bad', + 'scores' => 'not-an-array', + ]); + } }