- 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> |
||
|---|---|---|
| .. | ||
| Content | ||
| Contracts | ||
| Phase | ||
| Plan | ||
| Session | ||
| State | ||
| Task | ||
| Template | ||
| AgentTool.php | ||
| README.md | ||
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
- A tool declares its dependencies in a
dependencies()method returningToolDependency[]. - When the tool is registered,
AgentToolRegistry::register()passes those dependencies toToolDependencyService. - On each call,
AgentToolRegistry::execute()callsToolDependencyService::validateDependencies()before invokinghandle(). - If any required dependency fails, a
MissingDependencyExceptionis thrown and the tool is never called. - 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:
contextExists('workspace_id', ...)— tenant isolation firstsessionState('session_id', ...)— session presence secondentityExists(...)— 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.