LoggingToolWrapper.php•5.26 kB
<?php
declare(strict_types=1);
namespace OpenFGA\MCP;
use Exception;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionParameter;
use function array_key_exists;
use function count;
use function is_array;
use function method_exists;
use function sprintf;
final class LoggingToolWrapper
{
/**
* @param object $tool
* @param string $methodName
* @return callable(mixed...): mixed
*/
public static function wrapTool(object $tool, string $methodName): callable
{
return static function (...$args) use ($tool, $methodName) {
$toolName = $tool::class . '::' . $methodName;
// Handle MCP-style argument passing (single associative array)
// If we have a single array argument with string keys, treat it as named parameters
$actualArgs = $args;
if (1 === count($args) && isset($args[0]) && is_array($args[0]) && ! array_is_list($args[0])) {
// It's an associative array - convert to positional arguments
$reflection = new ReflectionClass($tool);
$method = $reflection->getMethod($methodName);
$parameters = $method->getParameters();
/** @var array<string, mixed> $namedArgs */
$namedArgs = $args[0];
$actualArgs = self::buildArgumentList($parameters, $namedArgs, $toolName);
}
// Convert args to int-keyed array for logging
/** @var array<int, mixed> $intKeyedArgs */
$intKeyedArgs = array_values($actualArgs);
try {
// Log the tool call with arguments
DebugLogger::logToolCall(
toolName: $toolName,
arguments: $intKeyedArgs,
result: null, // Will be filled after execution
id: null,
);
// Call the original method using reflection for proper type handling
if (method_exists($tool, $methodName)) {
try {
$reflection = new ReflectionClass($tool);
$method = $reflection->getMethod($methodName);
// Check if method is accessible (public)
if (! $method->isPublic()) {
throw new Exception(sprintf('Cannot access non-public method %s on %s', $methodName, $tool::class));
}
// ReflectionMethod::invoke returns mixed, which is expected for dynamic tool calls
/** @var mixed $result */
$result = $method->invoke($tool, ...$actualArgs);
} catch (ReflectionException $e) {
throw new Exception(sprintf('Failed to invoke method %s on %s: %s', $methodName, $tool::class, $e->getMessage()), $e->getCode(), $e);
}
} else {
throw new Exception(sprintf('Method %s not found on %s', $methodName, $tool::class));
}
// Log successful result
DebugLogger::logToolCall(
toolName: $toolName,
arguments: $intKeyedArgs,
result: $result,
id: null,
);
return $result;
} catch (Exception $exception) {
// Log error
DebugLogger::logError(
error: $exception->getMessage(),
id: null,
context: [
'tool' => $toolName,
'arguments' => $intKeyedArgs,
'exception_class' => $exception::class,
'trace' => $exception->getTraceAsString(),
],
);
// Re-throw the exception
throw $exception;
}
};
}
/**
* Build argument list from named parameters.
*
* @param list<ReflectionParameter> $parameters
* @param array<string, mixed> $namedArgs
* @param string $toolName
* @return list<mixed>
*/
private static function buildArgumentList(array $parameters, array $namedArgs, string $toolName): array
{
$result = [];
foreach ($parameters as $parameter) {
$paramName = $parameter->getName();
if (array_key_exists($paramName, $namedArgs)) {
/** @var mixed $value */
$value = $namedArgs[$paramName];
/** @var list<mixed> $result */
$result = [...$result, $value];
} elseif ($parameter->isDefaultValueAvailable()) {
/** @var mixed $defaultValue */
$defaultValue = $parameter->getDefaultValue();
/** @var list<mixed> $result */
$result = [...$result, $defaultValue];
} else {
throw new Exception(sprintf('Missing required parameter "%s" for %s', $paramName, $toolName));
}
}
return $result;
}
}