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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions