Custom Tools
SqlAgent ships with a fixed set of built-in tools (SQL execution, schema introspection, knowledge search, and learning). You can register additional tools so the LLM can call your own logic during the agentic loop.
Custom tools are plain PHP classes that extend Prism\Prism\Tool. They are resolved from the Laravel container, so constructor dependencies are injected automatically.
Registering Custom Tools
Section titled “Registering Custom Tools”List your tool class names in the agent.tools array in config/sql-agent.php:
'agent' => [ // ... other options ... 'tools' => [ \App\SqlAgent\CurrentDateTimeTool::class, \App\SqlAgent\FormatCurrencyTool::class, ],],Each class is resolved via app()->make(), so any constructor dependencies are injected by the container. Custom tools appear alongside the built-in tools — the LLM sees all of them and can call any tool on each iteration.
Creating a Custom Tool
Section titled “Creating a Custom Tool”A custom tool must extend Prism\Prism\Tool. Use the fluent API in the constructor to declare the tool’s name, description, and parameters. Then pass $this to ->using() and implement the logic in an __invoke method. This is the same pattern the built-in tools use.
<?php
namespace App\SqlAgent;
use Prism\Prism\Tool;
class CurrentDateTimeTool extends Tool{ public function __construct() { $this ->as('current_datetime') ->for('Get the current date and time. Use this when the user asks questions involving relative dates like "today", "this week", or "last month".') ->withStringParameter('timezone', 'IANA timezone (e.g. UTC, America/New_York). Defaults to UTC.', required: false) ->using($this); }
public function __invoke(?string $timezone = null): string { $tz = new \DateTimeZone($timezone ?? config('app.timezone', 'UTC')); $now = new \DateTimeImmutable('now', $tz);
return json_encode([ 'datetime' => $now->format('Y-m-d H:i:s'), 'date' => $now->format('Y-m-d'), 'time' => $now->format('H:i:s'), 'timezone' => $tz->getName(), 'day_of_week' => $now->format('l'), ], JSON_THROW_ON_ERROR); }}Tool API Reference
Section titled “Tool API Reference”The fluent methods available on Prism\Prism\Tool:
| Method | Description |
|---|---|
as(string $name) | Internal tool name the LLM uses to call it (snake_case recommended). |
for(string $description) | Description shown to the LLM — explain when to use the tool. |
using(callable $fn) | The handler to invoke. Pass $this for the __invoke pattern. |
withStringParameter(name, description, required) | Add a string parameter. |
withNumberParameter(name, description, required) | Add a numeric parameter. |
withBooleanParameter(name, description, required) | Add a boolean parameter. |
withEnumParameter(name, description, values, required) | Add a parameter limited to specific values. |
withArrayParameter(name, description, schema, required) | Add an array parameter with an item schema. |
withParameter(Schema $schema, required) | Add a parameter with a custom schema object. |
Dependency Injection
Section titled “Dependency Injection”Because tools are resolved from the container, you can type-hint services in the constructor:
class LookupExchangeRateTool extends Tool{ public function __construct(private ExchangeRateService $rates) { $this ->as('lookup_exchange_rate') ->for('Get the current exchange rate between two currencies') ->withStringParameter('from', 'Source currency code') ->withStringParameter('to', 'Target currency code') ->using($this); }
public function __invoke(string $from, string $to): string { return (string) $this->rates->getRate($from, $to); }}Return Values
Section titled “Return Values”Tool handlers must return a string. The LLM receives this string as the tool result and uses it to formulate its answer. For structured data, return JSON:
public function __invoke(string $code): string{ $rate = $this->rates->getRate($code);
return json_encode([ 'currency' => $code, 'rate' => $rate, 'updated_at' => now()->toIso8601String(), ], JSON_THROW_ON_ERROR);}If the tool throws an exception, the agent’s error handler captures it and reports the error message back to the LLM, which may retry or adjust its approach.
Validation
Section titled “Validation”SqlAgent validates custom tools at boot time:
- If a configured class does not exist, an
InvalidArgumentExceptionis thrown with a clear message. - If a class does not extend
Prism\Prism\Tool, anInvalidArgumentExceptionis thrown.
These errors surface immediately when the application boots, not at query time, so misconfigurations are caught early.
MCP Server Tools (Relay)
Section titled “MCP Server Tools (Relay)”SqlAgent integrates with Prism Relay to bring tools from MCP (Model Context Protocol) servers into the agentic loop. This lets you connect external tool servers — filesystem access, API wrappers, code interpreters, or any MCP-compatible server — without writing custom PHP tool classes.
Installation
Section titled “Installation”Install the Relay package:
composer require prism-php/relayThen publish and configure your MCP servers in config/relay.php following the Relay documentation.
Registering MCP Server Tools
Section titled “Registering MCP Server Tools”List the MCP server names (as defined in config/relay.php) in the agent.relay array in config/sql-agent.php:
'agent' => [ // ... other options ... 'tools' => [],
'relay' => [ 'weather-server', 'filesystem-server', ],],At boot time, SqlAgent calls Relay::tools($server) for each configured server and registers all discovered tools alongside the built-in and custom tools. The LLM sees and can call all of them.
Combining Custom Tools and Relay
Section titled “Combining Custom Tools and Relay”Custom tools and Relay tools can be used together. They all end up in the same tool registry and are passed to the LLM equally:
'agent' => [ 'tools' => [ \App\SqlAgent\CurrentDateTimeTool::class, ], 'relay' => [ 'weather-server', ],],If a Relay tool has the same name as a built-in or custom tool, it will overwrite the previous registration (last write wins).
- Keep tools focused. Each tool should do one thing well. Prefer two small tools over one tool with a
modeparameter. - Return JSON. Structured output helps the LLM parse results reliably.
- Be explicit in descriptions. The LLM decides which tool to call based on the
for()description. Vague descriptions lead to tools being called incorrectly or not at all. - Test your tools. Custom tools can be unit-tested independently — they’re just classes with an
__invokemethod.