Adding a tool
The rule
Section titled “The rule”An MCP tool is a thin connector. It only validates input, calls the same
domain class the Arkana screen uses, picks fields and returns them. It never
queries, recomputes, re-derives or re-labels anything. If the logic lives in a
Filament resource or page, move it to app/Services/ first and make the screen
consume it too.
The team rule for agents is backend/.ai/rules/mcp.md.
Checklist
Section titled “Checklist”-
Domain class. Put (or move) the logic in
app/Services/{Domain}/and make the Arkana screen use it. Reuse shared definitions such asBooking::scopeVisibleTo()andUser::canViewInternalPricing()instead of re-checking access. -
Ability. For a new domain, add a case to
App\Enums\McpAbility({domain}:reador{domain}:write) mapped to the Spatie permissions the admin screen requires. -
Tool class. Create
app/Mcp/Tools/{Domain}/{Verb}{Object}Tool.phpextendingApp\Mcp\Tools\ArkanaTool, implementingability(), with:#[Name('{domain}_{verb}[_{object}]')];#[IsReadOnly]or#[IsDestructive];#[Description]written for an LLM: name the Arkana screen it matches, state units and currency, and spell out caveats (empty states, which tool to call first).
-
Schemas. Implement
schema()andoutputSchema(). -
Validation. Use
$request->validate()with actionable messages (list the allowed values, show the expected format). -
Response. Return
Response::structured([...]), or$this->paginated()for lists ({data, next_cursor}). Use$this->notFound()instead offindOrFail()and$this->asDisplayed()to round figures. -
Register the tool in
App\Mcp\Servers\ArkanaServer::$tools. -
Tests in
tests/Feature/Mcp/{Domain}/:- a parity test asserting the tool output equals the domain class output
for the same record (see
GetBookingCostBreakdownToolTest); - access tests for anything the tool gates itself (e.g. supplier managers
denied pricing); token-ability and permission gating is covered once in
tests/Feature/Mcp/ArkanaServerTest.php.
Authenticate with
Sanctum::actingAs(mcpAgent(), [ability]); plainactingAs()does not represent a token and the tool will not register. - a parity test asserting the tool output equals the domain class output
for the same record (see
-
PHPStan. Run it from
backend/(./vendor/bin/sail composer analyse). -
Docs. Add the tool to the catalogue in the overview.
What the PHPStan rules reject
Section titled “What the PHPStan rules reject”Both rules are registered in backend/phpstan.neon and tested in
backend/tests/Unit/PHPStan/.
| Identifier | Rejects in App\Mcp |
|---|---|
volare.mcp.dataAccess |
Static calls on Eloquent models, the DB facade, calls on query builders or relations |
volare.mcp.arithmetic |
+ - * / % ** (and assignment/increment forms), math and format functions (round, number_format, array_sum, bc*, …); the only exception is round() inside ArkanaTool::asDisplayed() |
volare.mcp.filament |
Importing anything from App\Filament |
volare.mcp.toolStructure |
A tool under App\Mcp\Tools\ that does not extend ArkanaTool, lacks #[Name], or lacks #[IsReadOnly] / #[IsDestructive] |
The limit: PHPStan guarantees there is no logic in app/Mcp. It cannot
prove the tool returns the same results as the screen; that is guaranteed by
consuming the same class plus the parity test.
Sources: backend/phpstan/Rules/ForbidLogicInMcpLayerRule.php,
backend/phpstan/Rules/RequireMcpToolStructureRule.php