<?php
declare(strict_types=1);
namespace Butschster\ContextGenerator\McpServer\Action\Tools;
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post;
use Butschster\ContextGenerator\McpServer\Tool\Exception\ToolExecutionException;
use Butschster\ContextGenerator\McpServer\Tool\ToolHandlerFactory;
use Butschster\ContextGenerator\McpServer\Tool\ToolProviderInterface;
use PhpMcp\Schema\Content\TextContent;
use PhpMcp\Schema\Result\CallToolResult;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
final readonly class ExecuteCustomToolAction
{
public function __construct(
private LoggerInterface $logger,
private ToolProviderInterface $toolProvider,
private ToolHandlerFactory $toolHandler,
) {}
#[Post(path: '/tools/call/{id}', name: 'tools.execute')]
public function __invoke(ServerRequestInterface $request): CallToolResult
{
$this->logger->info('Executing custom tool');
$toolId = $request->getAttribute('id');
// Get params from the parsed body for POST requests
$parsedBody = (array) $request->getParsedBody();
if (empty($toolId) || !\is_string($toolId)) {
$this->logger->warning('No tool ID provided');
return new CallToolResult(
content: [new TextContent(text: 'No tool ID provided')],
isError: true,
);
}
try {
// Check if the tool exists
if (!$this->toolProvider->has($toolId)) {
$this->logger->warning('Tool not found', ['id' => $toolId]);
return new CallToolResult(
content: [new TextContent(text: \sprintf('Tool with ID "%s" not found', $toolId))],
isError: true,
);
}
// Get the tool definition
$tool = $this->toolProvider->get($toolId);
// Execute the tool
$result = $this->toolHandler->createHandlerForTool($tool)->execute($tool, $parsedBody);
// Format the result
$output = \sprintf(
"Executed tool: %s\n\n%s",
$tool->description,
$result['output'] ?? 'No output',
);
if (isset($result['commands']) && \is_array($result['commands'])) {
$commandsOutput = [];
foreach ($result['commands'] as $commandResult) {
$commandsOutput[] = \sprintf(
"Command: %s\nExit Code: %d\nSuccess: %s\n\n%s",
$commandResult['command'],
$commandResult['exitCode'],
$commandResult['success'] ? 'Yes' : 'No',
$commandResult['output'] ?? 'No output',
);
}
if (!empty($commandsOutput)) {
$output .= "\n\n## Command Details\n\n" . \implode("\n---\n", $commandsOutput);
}
}
return new CallToolResult(
content: [new TextContent(text: $output)],
isError: !($result['success'] ?? true),
);
} catch (ToolExecutionException $e) {
$this->logger->error('Tool execution failed', [
'id' => $toolId,
'error' => $e->getMessage(),
]);
return new CallToolResult(
content: [new TextContent(text: \sprintf('Tool execution failed: %s', $e->getMessage()))],
isError: true,
);
} catch (\Throwable $e) {
$this->logger->error('Unexpected error during tool execution', [
'id' => $toolId,
'error' => $e->getMessage(),
'exception' => $e::class,
]);
return new CallToolResult(
content: [new TextContent(text: \sprintf('Unexpected error: %s', $e->getMessage()))],
isError: true,
);
}
}
}