Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions php/Boot.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Core\Events\AdminPanelBooting;
use Core\Events\ApiRoutesRegistering;
use Core\Events\ConsoleBooting;
use Core\Events\McpToolsRegistering;
use Core\Mod\Agentic\Services\AgenticManager;
use Core\Mod\Agentic\Services\AgentResourceRegistry;
use Core\Mod\Agentic\Services\AgentToolRegistry;
Expand All @@ -34,7 +33,6 @@ class Boot extends ServiceProvider
AdminPanelBooting::class => 'onAdminPanel',
ApiRoutesRegistering::class => 'onApiRoutes',
ConsoleBooting::class => 'onConsole',
McpToolsRegistering::class => 'onMcpTools',
];

public function boot(): void
Expand Down Expand Up @@ -98,6 +96,8 @@ public function register(): void
$this->app->singleton(AgenticManager::class);
$this->app->singleton(AgentToolRegistry::class);

$this->registerAgentTools();

// Resources are bound here rather than hung off an event the way tools
// are. There is no McpResourcesRegistering to listen for, and $listens
// is populated by ModuleScanner from app/Core|Mod|Website only — dead
Expand Down Expand Up @@ -215,16 +215,30 @@ public function onConsole(ConsoleBooting $event): void
}

/**
* Handle MCP tools registration event.
* Fill the tool registry.
*
* Called from register(), not from the McpToolsRegistering event this used
* to listen for. $listens is populated by ModuleScanner scanning
* app/Core|Mod|Website only, so it is dead once this package is installed
* under vendor/: the event never fired and the registry stayed empty, which
* is half of why the stdio server advertised no tools. The other half was
* that it read a different registry entirely — see listTools() on the class
* this fills.
*
* Note: Agent tools (plan_create, session_start, etc.) are implemented in
* the Mcp module at Mod\Mcp\Tools\Agent\* and registered via AgentToolRegistry.
* Brain tools are registered here as they belong to the Agentic module.
* @example
* $this->registerAgentTools();
*/
public function onMcpTools(McpToolsRegistering $event): void
private function registerAgentTools(): void
{
$registry = $this->app->make(AgentToolRegistry::class);

// Idempotent: register() runs once per application, but a host that
// still delivers the event must not double-register and trip the
// duplicate-name guard.
if ($registry->all()->isNotEmpty()) {
return;
}
Comment on lines +235 to +240

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Register missing built-in tools when the registry has custom tools.

At Line 238, one unrelated pre-registered tool makes this method return. If a host registers a custom tool before this provider, the built-in MCP tools are never registered.

Check registration per built-in tool. Keep the duplicate-name failure when a different implementation claims a built-in name.

Proposed fix
-        if ($registry->all()->isNotEmpty()) {
-            return;
-        }
-
         $toolClasses = [
             // ...
         ];
 
-        $registry->registerMany(array_map(
-            static fn (string $toolClass) => new $toolClass,
-            $toolClasses,
-        ));
+        foreach ($toolClasses as $toolClass) {
+            $tool = new $toolClass;
+            $existing = $registry->get($tool->name());
+
+            if ($existing === null) {
+                $registry->register($tool);
+                continue;
+            }
+
+            if ($existing::class !== $tool::class) {
+                throw new \InvalidArgumentException(sprintf(
+                    'Tool [%s] is already registered by [%s].',
+                    $tool->name(),
+                    $existing::class,
+                ));
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@php/Boot.php` around lines 235 - 240, Update the registration logic in
Boot.php so it checks each built-in tool name individually instead of returning
whenever Registry::all() is non-empty. Register any missing built-in tools even
when custom tools already exist, while preserving the duplicate-name failure
when an existing tool with a built-in name is a different implementation.


$toolClasses = [
Mcp\Tools\Agent\Brain\BrainRemember::class,
Mcp\Tools\Agent\Brain\BrainRecall::class,
Expand Down
10 changes: 5 additions & 5 deletions php/Mcp/Console/McpAgentServerCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@

use Core\Mod\Agentic\Mcp\Services\McpQuotaService;
use Core\Mod\Agentic\Mcp\Services\QueryAuditService;
use Core\Mod\Agentic\Mcp\Services\ToolRegistry;
use Core\Mod\Agentic\Services\AgentResourceRegistry;
use Core\Mod\Agentic\Services\AgentToolRegistry;
use Illuminate\Console\Command;
use InvalidArgumentException;
use JsonException;
Expand All @@ -35,7 +35,7 @@ class McpAgentServerCommand extends Command
* $exitCode = $this->handle($toolRegistry, $quotaService, $queryAuditService);
*/
public function handle(
ToolRegistry $toolRegistry,
AgentToolRegistry $toolRegistry,
McpQuotaService $quotaService,
QueryAuditService $queryAuditService,
AgentResourceRegistry $resourceRegistry,
Expand Down Expand Up @@ -118,7 +118,7 @@ private function streamPath(string $variable, string $default): string
*/
private function processPayload(
string $payload,
ToolRegistry $toolRegistry,
AgentToolRegistry $toolRegistry,
McpQuotaService $quotaService,
QueryAuditService $queryAuditService,
AgentResourceRegistry $resourceRegistry,
Expand Down Expand Up @@ -174,7 +174,7 @@ private function processPayload(
*/
private function processRequest(
array $request,
ToolRegistry $toolRegistry,
AgentToolRegistry $toolRegistry,
McpQuotaService $quotaService,
QueryAuditService $queryAuditService,
AgentResourceRegistry $resourceRegistry,
Expand Down Expand Up @@ -262,7 +262,7 @@ private function processRequest(
private function handleToolCall(
array $params,
mixed $id,
ToolRegistry $toolRegistry,
AgentToolRegistry $toolRegistry,
McpQuotaService $quotaService,
QueryAuditService $queryAuditService,
): array {
Expand Down
9 changes: 5 additions & 4 deletions php/Mcp/Services/ToolDependencyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use Carbon\CarbonImmutable;
use Core\Mcp\Dependencies\ToolDependency;
use Core\Mod\Agentic\Services\AgentToolRegistry;
use Illuminate\Container\Container;
use InvalidArgumentException;
use RuntimeException;
Expand All @@ -25,7 +26,7 @@ final class ToolDependencyService
private array $toolCalls = [];

public function __construct(
private ?ToolRegistry $registry = null,
private ?AgentToolRegistry $registry = null,
private readonly ?Container $container = null,
) {
$this->registry ??= $this->resolveRegistry();
Expand Down Expand Up @@ -105,15 +106,15 @@ public function calledTools(string $sessionId): array
return array_keys($this->toolCalls[$sessionId] ?? []);
}

private function resolveRegistry(): ?ToolRegistry
private function resolveRegistry(): ?AgentToolRegistry
{
$container = $this->container ?? Container::getInstance();

if (! $container instanceof Container || ! $container->bound(ToolRegistry::class)) {
if (! $container instanceof Container || ! $container->bound(AgentToolRegistry::class)) {
return null;
}

return $container->make(ToolRegistry::class);
return $container->make(AgentToolRegistry::class);
}

/**
Expand Down
88 changes: 0 additions & 88 deletions php/Mcp/Services/ToolRegistry.php

This file was deleted.

88 changes: 88 additions & 0 deletions php/Services/AgentToolRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Core\Api\Models\ApiKey;
use Core\Mcp\Dependencies\HasDependencies;
use Core\Mcp\Exceptions\MissingDependencyException;
use Core\Mod\Agentic\Mcp\Data\ToolMetadata;
use Core\Mod\Agentic\Mcp\Services\ToolDependencyService;
use Core\Mod\Agentic\Mcp\Tools\Agent\Contracts\AgentToolInterface;
use Illuminate\Support\Collection;
Expand Down Expand Up @@ -37,6 +38,16 @@ class AgentToolRegistry
*/
public function register(AgentToolInterface $tool): self
{
// Absorbed from the registry this replaced: two tools claiming one name
// is a wiring mistake, and silently keeping the last one registered
// means the MCP surface serves whichever file happened to load second.
if (isset($this->tools[$tool->name()])) {
throw new \InvalidArgumentException(sprintf(
'Tool [%s] is already registered.',
$tool->name(),
));
}

$this->tools[$tool->name()] = $tool;

// Auto-register dependencies if tool declares them
Expand Down Expand Up @@ -363,4 +374,81 @@ private function enforceAndRecordRateLimit(ApiKey $apiKey, string $toolName): vo
);
}
}

/**
* Every registered tool as MCP metadata.
*
* Absorbed from Mcp\Services\ToolRegistry, which the stdio agent server
* read while Boot filled this registry instead — so the server listed
* nothing. One registry now, so there is one answer to "what tools exist".
*
* @return array<int, ToolMetadata>
*
* @example
* $registry->listTools();
*/
public function listTools(): array
{
return array_values(array_map(
static fn (AgentToolInterface $tool): ToolMetadata => ToolMetadata::from($tool),
$this->tools,
));
}

/**
* Resolve one tool as MCP metadata, or null when it is not registered.
*
* @example
* $registry->resolve('plan_create');
*/
public function resolve(string $name): ?ToolMetadata
{
$tool = $this->tools[$name] ?? null;

return $tool === null ? null : ToolMetadata::from($tool);
}

/**
* Map each tool name to the identifiers it declares as dependencies.
*
* @return array<string, array<int, string>>
*
* @example
* $registry->buildDependencyGraph();
*/
public function buildDependencyGraph(): array
{
$graph = [];

foreach ($this->tools as $name => $tool) {
$graph[$name] = ToolMetadata::from($tool)->dependencyIdentifiers();
}

return $graph;
}

/**
* Invoke a tool directly, without the permission and dependency checks
* execute() applies.
*
* Kept distinct from execute() rather than merged into it: the stdio
* transport has no API key to check scopes against and runs its own quota
* and audit passes around this call, whereas execute() is the governed
* path used where an ApiKey is present.
*
* @throws \InvalidArgumentException If the tool is not registered
*
* @example
* $registry->call('plan_list', [], ['workspace_id' => 'ws-1']);
*/
public function call(string $name, array $arguments = [], array $context = []): mixed
{
$tool = $this->get($name);

if (! $tool) {
throw new \InvalidArgumentException(sprintf('Unknown tool [%s].', $name));
}

return $tool->handle($arguments, $context);
Comment on lines +430 to +452

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline php/Mcp/Console/McpAgentServerCommand.php --match handleToolCall --view expanded

rg -n -C 6 \
  'function handleToolCall|validateDependencies|recordToolCall|\$toolRegistry->call' \
  php/Mcp/Console/McpAgentServerCommand.php \
  php/Mcp/Services/ToolDependencyService.php

Repository: dAppCore/agent

Length of output: 5562


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '232,325p' php/Mcp/Console/McpAgentServerCommand.php
printf '\n--- Tool dependency service full missing methods ---\n'
sed -n '1,135p' php/Mcp/Services/ToolDependencyService.php
printf '\n--- AgentToolRegistry outline and relevant sections ---\n'
ast-grep outline php/Services/AgentToolRegistry.php --view expanded
sed -n '1,140p' php/Services/AgentToolRegistry.php
sed -n '390,470p' php/Services/AgentToolRegistry.php

Repository: dAppCore/agent

Length of output: 14941


Add dependency validation and call recording to handleToolCall().

The stdio tools/call path calls $toolRegistry->call() directly, while call() intentionally skips dependency checks. Add ToolDependencyService::validateDependencies(...) before the invocation and recordToolCall(...) after a successful call so MCP clients cannot bypass dependency ordering and tool-call recording.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@php/Services/AgentToolRegistry.php` around lines 430 - 452, Update
AgentToolRegistry::call() to validate dependencies through
ToolDependencyService::validateDependencies(...) before invoking the tool, then
record the call with recordToolCall(...) only after handle() succeeds. Preserve
the existing unknown-tool exception and direct invocation behavior while
ensuring the stdio tools/call path cannot bypass dependency checks or recording.

}
}
15 changes: 13 additions & 2 deletions php/tests/Feature/Mcp/Console/McpAgentServerCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

use Core\Mod\Agentic\Mcp\Console\McpAgentServerCommand;
use Core\Mod\Agentic\Mcp\Services\McpQuotaService;
use Core\Mod\Agentic\Mcp\Services\ToolRegistry;
use Core\Mod\Agentic\Mcp\Tools\Agent\Contracts\AgentToolInterface;
use Core\Mod\Agentic\Models\AgentPlan;
use Core\Mod\Agentic\Services\AgentToolRegistry;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
Expand All @@ -19,7 +20,7 @@
$this->app->make(McpAgentServerCommand::class),
);

ToolRegistry::registerSingleton($this->app)->register(new class
$this->app->make(AgentToolRegistry::class)->register(new class implements AgentToolInterface
{
public function name(): string
{
Expand All @@ -44,6 +45,16 @@ public function handle(array $arguments, array $context = []): array
'value' => $arguments['value'] ?? null,
];
}

public function requiredScopes(): array
{
return ['read'];
}

public function category(): string
{
return 'testing';
}
});

Schema::dropIfExists('mcp_audit_entries');
Expand Down
Loading
Loading