php-agentic/Mcp/Tools/Agent
darbs-claude 7fadbcb96c
Some checks failed
CI / PHP 8.3 (pull_request) Failing after 1s
CI / PHP 8.4 (pull_request) Failing after 1s
refactor: consolidate duplicate state models into WorkspaceState (#18)
- Delete Models/AgentWorkspaceState.php (legacy port, no backing table)
- Rewrite Models/WorkspaceState.php as the single canonical state model
  backed by agent_workspace_states table with array value cast,
  type helpers, scopeForPlan/scopeOfType, static getValue/setValue,
  and toMcpContext() for MCP tool output
- Update AgentPlan::states() relation and setState() return type
- Update StateSet MCP tool import
- Update SecurityTest to use WorkspaceState
- Add WorkspaceStateTest covering table, casts, type helpers, scopes,
  static helpers, toMcpContext, and AgentPlan integration
- Mark CQ-001 done in TODO.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 13:26:23 +00:00
..
Content chore: fix pint code style and add test config 2026-02-23 03:50:09 +00:00
Contracts refactor: rename namespace Core\Agentic to Core\Mod\Agentic 2026-01-27 16:12:58 +00:00
Phase chore: fix pint code style and add test config 2026-02-23 03:50:09 +00:00
Plan fix: improve workspace context error messages (closes #28) 2026-02-23 11:28:32 +00:00
Session fix: improve workspace context error messages (closes #28) 2026-02-23 11:28:32 +00:00
State refactor: consolidate duplicate state models into WorkspaceState (#18) 2026-02-24 13:26:23 +00:00
Task refactor: update namespaces for L1/L2 package convention 2026-01-27 17:34:46 +00:00
Template chore: fix pint code style and add test config 2026-02-23 03:50:09 +00:00
AgentTool.php refactor: update namespaces for L1/L2 package convention 2026-01-27 17:34:46 +00:00
README.md docs: document MCP tool dependency system 2026-02-23 12:05:35 +00:00

MCP Agent Tools

This directory contains MCP (Model Context Protocol) tool implementations for the agent orchestration system. All tools extend AgentTool and integrate with the ToolDependency system to declare and validate their execution prerequisites.

Directory Structure

Mcp/Tools/Agent/
├── AgentTool.php              # Base class — extend this for all new tools
├── Contracts/
│   └── AgentToolInterface.php # Tool contract
├── Content/                   # Content generation tools
├── Phase/                     # Plan phase management tools
├── Plan/                      # Work plan CRUD tools
├── Session/                   # Agent session lifecycle tools
├── State/                     # Shared workspace state tools
├── Task/                      # Task status and tracking tools
└── Template/                  # Template listing and application tools

ToolDependency System

ToolDependency (from Core\Mcp\Dependencies\ToolDependency) lets a tool declare what must be true in the execution context before it runs. The AgentToolRegistry validates these automatically — the tool's handle() method is never called if a dependency is unmet.

How It Works

  1. A tool declares its dependencies in a dependencies() method returning ToolDependency[].
  2. When the tool is registered, AgentToolRegistry::register() passes those dependencies to ToolDependencyService.
  3. On each call, AgentToolRegistry::execute() calls ToolDependencyService::validateDependencies() before invoking handle().
  4. If any required dependency fails, a MissingDependencyException is thrown and the tool is never called.
  5. After a successful call, ToolDependencyService::recordToolCall() logs the execution for audit purposes.

Dependency Types

contextExists — Require a context field

Validates that a key is present in the $context array passed at execution time. Use this for multi-tenant isolation fields like workspace_id that come from API key authentication.

ToolDependency::contextExists('workspace_id', 'Workspace context required')

Mark a dependency optional with ->asOptional() when the tool can work without it (e.g. the value can be inferred from another argument):

// SessionStart: workspace can be inferred from the plan if plan_slug is provided
ToolDependency::contextExists('workspace_id', 'Workspace context required (or provide plan_slug)')
    ->asOptional()

sessionState — Require an active session

Validates that a session is active. Use this for tools that must run within an established session context.

ToolDependency::sessionState('session_id', 'Active session required. Call session_start first.')

entityExists — Require a database entity

Validates that an entity exists in the database before the tool runs. The arg_key maps to the tool argument that holds the entity identifier.

ToolDependency::entityExists('plan', 'Plan must exist', ['arg_key' => 'plan_slug'])

Context Requirements

The $context array is injected into every tool's handle(array $args, array $context) call. Context is set by API key authentication middleware — tools should never hardcode or fall back to default values.

Key Type Set by Used by
workspace_id string|int API key auth middleware All workspace-scoped tools
session_id string Client (from session_start response) Session-dependent tools

Multi-tenant safety: Always validate workspace_id in handle() as a defence-in-depth measure, even when a contextExists dependency is declared. Use forWorkspace($workspaceId) scopes on all queries.

$workspaceId = $context['workspace_id'] ?? null;
if ($workspaceId === null) {
    return $this->error('workspace_id is required. Ensure you have authenticated with a valid API key. See: https://host.uk.com/ai');
}

$plan = AgentPlan::forWorkspace($workspaceId)->where('slug', $slug)->first();

Creating a New Tool

1. Create the class

Place the file in the appropriate subdirectory and extend AgentTool:

<?php

declare(strict_types=1);

namespace Core\Mod\Agentic\Mcp\Tools\Agent\Plan;

use Core\Mcp\Dependencies\ToolDependency;
use Core\Mod\Agentic\Mcp\Tools\Agent\AgentTool;

class PlanPublish extends AgentTool
{
    protected string $category = 'plan';

    protected array $scopes = ['write']; // 'read' or 'write'

    public function dependencies(): array
    {
        return [
            ToolDependency::contextExists('workspace_id', 'Workspace context required'),
        ];
    }

    public function name(): string
    {
        return 'plan_publish'; // snake_case; must be unique across all tools
    }

    public function description(): string
    {
        return 'Publish a draft plan, making it active';
    }

    public function inputSchema(): array
    {
        return [
            'type' => 'object',
            'properties' => [
                'plan_slug' => [
                    'type' => 'string',
                    'description' => 'Plan slug identifier',
                ],
            ],
            'required' => ['plan_slug'],
        ];
    }

    public function handle(array $args, array $context = []): array
    {
        try {
            $planSlug = $this->requireString($args, 'plan_slug', 255);
        } catch (\InvalidArgumentException $e) {
            return $this->error($e->getMessage());
        }

        $workspaceId = $context['workspace_id'] ?? null;
        if ($workspaceId === null) {
            return $this->error('workspace_id is required. See: https://host.uk.com/ai');
        }

        $plan = AgentPlan::forWorkspace($workspaceId)->where('slug', $planSlug)->first();

        if (! $plan) {
            return $this->error("Plan not found: {$planSlug}");
        }

        $plan->update(['status' => 'active']);

        return $this->success(['plan' => ['slug' => $plan->slug, 'status' => $plan->status]]);
    }
}

2. Register the tool

Add it to the tool registration list in the package boot sequence (see Boot.php and the McpToolsRegistering event handler).

3. Write tests

Add a Pest test file under Tests/ covering success and failure paths, including missing dependency scenarios.

AgentTool Base Class Reference

Properties

Property Type Default Description
$category string 'general' Groups tools in the registry
$scopes string[] ['read'] API key scopes required to call this tool
$timeout ?int null Per-tool timeout override in seconds (null uses config default of 30s)

Argument Helpers

All helpers throw \InvalidArgumentException on failure. Catch it in handle() and return $this->error().

Method Description
requireString($args, $key, $maxLength, $label) Required string with optional max length
requireInt($args, $key, $min, $max, $label) Required integer with optional bounds
requireArray($args, $key, $label) Required array
requireEnum($args, $key, $allowed, $label) Required string constrained to allowed values
optionalString($args, $key, $default, $maxLength) Optional string
optionalInt($args, $key, $default, $min, $max) Optional integer
optionalEnum($args, $key, $allowed, $default) Optional enum string
optional($args, $key, $default) Optional value of any type

Response Helpers

return $this->success(['key' => 'value']); // merges ['success' => true]
return $this->error('Something went wrong');
return $this->error('Resource locked', 'resource_locked'); // with error code

Circuit Breaker

Wrap calls to external services with withCircuitBreaker() for fault tolerance:

return $this->withCircuitBreaker(
    'agentic',                                                    // service name
    fn () => $this->doWork(),                                     // operation
    fn () => $this->error('Service unavailable', 'service_unavailable') // fallback
);

If no fallback is provided and the circuit is open, error() is returned automatically.

Timeout Override

For long-running tools (e.g. content generation), override the timeout:

protected ?int $timeout = 300; // 5 minutes

Dependency Resolution Order

Dependencies are validated in the order they are returned from dependencies(). All required dependencies must pass before the tool runs. Optional dependencies are checked but do not block execution.

Recommended declaration order:

  1. contextExists('workspace_id', ...) — tenant isolation first
  2. sessionState('session_id', ...) — session presence second
  3. entityExists(...) — entity existence last (may query DB)

Troubleshooting

"Workspace context required"

The workspace_id key is missing from the execution context. This is injected by the API key authentication middleware. Causes:

  • Request is unauthenticated or the API key is invalid.
  • The API key has no workspace association.
  • Dependency validation was bypassed but the tool checks it internally.

Fix: Authenticate with a valid API key. See https://host.uk.com/ai.

"Active session required. Call session_start first."

The session_id context key is missing. The tool requires an active session.

Fix: Call session_start before calling session-dependent tools. Pass the returned session_id in the context of all subsequent calls.

"Plan must exist" / "Plan not found"

The plan_slug argument does not match any plan. Either the plan was never created, the slug is misspelled, or the plan belongs to a different workspace.

Fix: Call plan_list to find valid slugs, then retry.

"Permission denied: API key missing scope"

The API key does not have the required scope (read or write) for the tool.

Fix: Issue a new API key with the correct scopes, or use an existing key that has the required permissions.

"Unknown tool: {name}"

The tool name does not match any registered tool.

Fix: Check plan_list / MCP tool discovery endpoint for the exact tool name. Names are snake_case.

MissingDependencyException in logs

A required dependency was not met and the framework threw before calling handle(). The exception message will identify which dependency failed.

Fix: Inspect the context passed to execute(). Ensure required keys are present and the relevant entity exists.