Skip to content

Bug: Invokable class handlers crash in RegisteredElement::handle() #46

@sergioalborada

Description

@sergioalborada

Registering a tool using an invokable class results in a runtime error when the tool is called:

ReflectionException(code: 0): Function App\\Mcp\\Tools\\AskNexaTool() does not exist at /var/www/html/vendor/php-mcp/server/src/Elements/RegisteredElement.php:36)

I tried to track down the error:

In ServerBuilder.php, the registration happens; note that it correctly uuses HandlerResolver::resolve() to inspect the handler:

foreach ($this->manualTools as $data) {
            try {
                $reflection = HandlerResolver::resolve($data['handler']);

                if ($reflection instanceof \ReflectionFunction) {
                    $name = $data['name'] ?? 'closure_tool_' . spl_object_id($data['handler']);
                    $description = $data['description'] ?? null;
                } else {
                    $classShortName = $reflection->getDeclaringClass()->getShortName();
                    $methodName = $reflection->getName();
                    $docBlock = $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null);

                    $name = $data['name'] ?? ($methodName === '__invoke' ? $classShortName : $methodName);
                    $description = $data['description'] ?? $docBlockParser->getSummary($docBlock) ?? null;
                }

                $inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);

                $tool = Tool::make($name, $inputSchema, $description, $data['annotations']);
                $registry->registerTool($tool, $data['handler'], true);

                $handlerDesc = $data['handler'] instanceof \Closure ? 'Closure' : (is_array($data['handler']) ? implode('::', $data['handler']) : $data['handler']);
                $logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
            } catch (Throwable $e) {
                $logger->error('Failed to register manual tool', ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e]);
                throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
            }
        }

But when registering the tool:

$registry->registerTool($tool, $data['handler'], true);

…the original handler string (App\Mcp\Tools\AskNexaTool) is passed down to RegisteredTool > RegisteredElement.

public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void
    {
        $toolName = $tool->name;
        $existing = $this->tools[$toolName] ?? null;

        if ($existing && ! $isManual && $existing->isManual) {
            $this->logger->debug("Ignoring discovered tool '{$toolName}' as it conflicts with a manually registered one.");

            return;
        }

        $this->tools[$toolName] = RegisteredTool::make($tool, $handler, $isManual);

        $this->checkAndEmitChange('tools', $this->tools);
    }

In RegisteredElement::handle(), this happens:

public function handle(ContainerInterface $container, array $arguments): mixed
    {
        if (is_string($this->handler)) {
            $reflection = new \ReflectionFunction($this->handler);  // <--- breaks here 
            $arguments = $this->prepareArguments($reflection, $arguments);
            $instance = $container->get($this->handler);
            return call_user_func($instance, ...$arguments);
        }
        ..... 
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions