agent/php/Mcp/Services/ToolRegistry.php
Snider 09054fbdab feat(mcp): implement §3 Services (ToolRegistry + McpQuotaService + QueryAuditService + ToolDependencyService) (#851)
Additive-only — no existing files modified.

- ToolRegistry: register/resolve/listTools/buildDependencyGraph
  - Singleton via registerSingleton() entry point (no Boot.php wire-in
    per scope; tests cover the binding path)
- McpQuotaService: workspace-scoped checkQuota/consume/reset
- QueryAuditService: log/query/aggregate (expects mcp_audit_entries
  table; tests create inline as migration was out-of-scope)
- ToolDependencyService: validateDependencies via graph traversal

Data DTOs: ToolMetadata, QuotaResult, AuditEntry as readonly.
Pest Feature tests _Good/_Bad/_Ugly per AX-10.
pest skipped (vendor binaries missing).

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=851
2026-04-25 05:14:15 +01:00

88 lines
2 KiB
PHP

<?php
// SPDX-License-Identifier: EUPL-1.2
declare(strict_types=1);
namespace Core\Mod\Agentic\Mcp\Services;
use Core\Mod\Agentic\Mcp\Data\ToolMetadata;
use Illuminate\Container\Container;
use InvalidArgumentException;
final class ToolRegistry
{
/**
* @var array<string, ToolMetadata>
*/
private array $tools = [];
public function __construct(
private readonly ?Container $container = null,
) {}
public static function registerSingleton(Container $container): self
{
if (! $container->bound(self::class)) {
$container->singleton(self::class, fn (Container $app): self => new self($app));
}
return $container->make(self::class);
}
public function register(mixed $tool): ToolMetadata
{
$metadata = ToolMetadata::from($tool);
if (isset($this->tools[$metadata->name])) {
throw new InvalidArgumentException(sprintf(
'Tool [%s] is already registered.',
$metadata->name,
));
}
$this->tools[$metadata->name] = $metadata;
return $metadata;
}
public function resolve(string $name): ?ToolMetadata
{
return $this->tools[$name] ?? null;
}
/**
* @return array<int, ToolMetadata>
*/
public function listTools(): array
{
return array_values($this->tools);
}
/**
* @return array<string, array<int, string>>
*/
public function buildDependencyGraph(): array
{
$graph = [];
foreach ($this->tools as $name => $tool) {
$graph[$name] = $tool->dependencyIdentifiers();
}
return $graph;
}
public function call(string $name, array $arguments = [], array $context = []): mixed
{
$tool = $this->resolve($name);
if ($tool === null) {
throw new InvalidArgumentException(sprintf(
'Unknown tool [%s].',
$name,
));
}
return $tool->call($arguments, $context);
}
}