diff --git a/Mcp/Tools/Agent/README.md b/Mcp/Tools/Agent/README.md new file mode 100644 index 0000000..8112c3e --- /dev/null +++ b/Mcp/Tools/Agent/README.md @@ -0,0 +1,279 @@ +# 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. + +```php +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): + +```php +// 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. + +```php +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. + +```php +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. + +```php +$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 + '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 + +```php +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: + +```php +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: + +```php +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.