# Tools

`boru/ai` uses a runtime-owned tool loop.

Providers may surface tool calls, but the runtime is responsible for executing tools, collecting tool results, and continuing generation.

For the chat callback surface that consumes tool lifecycle events, see `chat.md`. For the architectural boundary behind this design, see `architecture.md`.

## Inline tools

```php
$response = AI::provider('gemini')
    ->model('gemini-2.5-pro')
    ->setToolContextValue('temp', 69)
    ->defineTool(
        'get_current_temperature',
        'Get the current temperature for a city',
        array(
            'type' => 'object',
            'properties' => array(
                'location' => array('type' => 'string'),
            ),
            'required' => array('location'),
        ),
        function (array $arguments) {
            return array(
                'location' => $arguments['location'],
                'temperature' => 69,
                'unit' => 'F',
            );
        }
    )
    ->addMessage('What is the temperature in Atlanta?')
    ->run();
```

## Class-based tools

For larger applications, class-based tools are cleaner.

Use:

- `ToolInterface`
- `AbstractTool`
- `ToolPromptAdditionInterface` when a tool should contribute prompt guidance only while it is enabled

Example:

```php
$ai->addTool(new WeatherTemperatureTool());
```

`boru/ai` also includes built-in conversation-memory tools that can be auto-exposed through config:

- `update_conversation_title`
- `update_conversation_summary`
- `append_conversation_note`
- `replace_conversation_notes`

For the full conversation-memory model and config knobs, see `conversation-memory.md`.

## Tool prompt additions

If a tool needs prompt-side guidance that should only apply while that tool is enabled, implement `ToolPromptAdditionInterface`.

That keeps tool activation and prompt guidance coupled in one place instead of forcing callers to coordinate both separately.

Example:

```php
use boru\ai\Runtime\AbstractTool;
use boru\ai\Runtime\ToolPromptAdditionInterface;

class SaveSummaryTool extends AbstractTool implements ToolPromptAdditionInterface
{
    public function name()
    {
        return 'save_summary';
    }

    public function promptAddition()
    {
        return 'When the conversation state changes materially, update the saved summary.';
    }

    public function handle(array $arguments, \boru\ai\Runtime\ToolExecutionContext $context = null)
    {
        return array('ok' => true);
    }
}
```

When that tool is active on the request, `boru/ai` merges the returned text into the effective instructions automatically.

## Legacy tool compatibility

`boru/ai` also includes compatibility helpers for older BoruAI-style tools.

This compatibility path is optional. If you want to adapt legacy BoruAI `FunctionBase` / `ToolDefinition` tools, install `boru/boruai` alongside `boru/ai`.

Use:

```php
$ai->addLegacyTool($legacyClassName);
```

This is intended as a bridge for migration, not as the preferred long-term tool style.

## MCP-backed tools

MCP tools are intended to be first-class runtime tools.

That means:

- providers should only see normal tool schemas
- the runtime should still own execution and continuation
- MCP servers should be registered before their tools are added

Example:

```php
use boru\ai\AI;
use boru\ai\Mcp\McpServerConfig;

AI::mcpServers()->register(new McpServerConfig(array(
    'name' => 'filesystem',
    'transport' => 'stdio',
    'command' => 'npx',
    'args' => array('-y', '@modelcontextprotocol/server-filesystem', '/tmp'),
)));

$response = AI::provider('openai')
    ->model('gpt-4.1')
    ->addMcpTool('filesystem', array(
        'name' => 'read_file',
        'description' => 'Read a file from disk',
        'inputSchema' => array(
            'type' => 'object',
            'properties' => array(
                'path' => array('type' => 'string'),
            ),
            'required' => array('path'),
        ),
    ))
    ->addMessage('Read /tmp/example.txt')
    ->run();
```

At the moment, MCP discovery and transport/session clients are still separate follow-on work. The current MCP path is for registering already-known remote tool metadata into the normal runtime tool registry.

When you do have an MCP discovery service configured, you can explicitly discover and advertise tools for a request:

```php
$ai = AI::provider('openai')
    ->model('gpt-4.1')
    ->addDiscoveredMcpTools('filesystem')
    ->addMessage('Read /tmp/example.txt');
```

That keeps discovery explicit at the application/runtime layer. Providers still only receive normal tool schemas, and MCP transport/authentication concerns remain behind MCP-specific services.

If you want MCP discovery to be best-effort instead of terminal, use `tryAddDiscoveredMcpTools()`. It suppresses discovery/setup exceptions, records them in request metadata under `mcp_discovery_errors`, and lets the AI request continue without MCP tools.

The client-side scaffolding in `boru/ai` now assumes the same broad session shape commonly used by `boru/mcp` and `boru/borumcp` proxy deployments:

- initialize a session first
- optionally provide auth during initialization
- list tools
- call tools

Those transport details are still intentionally hidden behind MCP client/discovery/invoker services rather than leaking into provider adapters or normal request tool schemas.

The first concrete transport path is an HTTP proxy client, which is a good fit for deployments exposing `/tools/list` and `/tools/call`. In that shape, your MCP server config can point at the proxy URL and carry any needed headers or bearer auth in the MCP config rather than in provider code.

For `boru/mcp` or `boru/borumcp` proxy endpoints specifically, the proxy may also require an `internal-port` header so it knows which upstream TCP MCP server to initialize. That header is proxy-specific configuration, not part of generic MCP itself, and does not require `boru/ai` to depend on the `boru/borumcp` package directly.

`boru/ai` also now includes a direct TCP MCP client path for servers that expose the raw session flow themselves. That path performs explicit `initialize`, then `tools/list` / `tools/call`, and carries authentication through MCP server config rather than provider code.

## Runtime tool lifecycle events

When you attach an event callback or use the fluent chat callback helpers, `boru/ai` emits normalized runtime-native tool lifecycle events during execution:

- `tool.execution.started`
- `tool.execution.completed`
- `tool.execution.failed`

These events give downstream consumers a coherent tool-call lifecycle shape without requiring provider-specific correlation glue.

Each runtime tool lifecycle event includes the same core payload fields:

- `tool_call_id`
- `tool_name`
- `arguments`
- `arguments_json`
- `output`
- `metadata`
- `status`
- `success`

## Tool callback example

```php
$response = AI::provider('gemini')
    ->model('gemini-2.5-pro')
    ->defineTool(
        'get_current_temperature',
        'Get the current temperature for a city',
        array(
            'type' => 'object',
            'properties' => array(
                'location' => array('type' => 'string'),
            ),
            'required' => array('location'),
        ),
        function (array $arguments) {
            return array(
                'location' => $arguments['location'],
                'temperature' => 69,
                'unit' => 'F',
            );
        }
    )
    ->addMessage('What is the temperature in Atlanta?')
    ->onToolStart(function ($toolCallId, $toolName, $arguments) {
        echo '[tool:start] ' . $toolName . PHP_EOL;
    })
    ->onToolComplete(function ($toolCallId, $toolName, $output) {
        echo '[tool:done] ' . $toolName . PHP_EOL;
    })
    ->onToolFailed(function ($toolCallId, $toolName, $output) {
        echo '[tool:failed] ' . $toolName . PHP_EOL;
    })
    ->run();
```

## Built-in tool call listener

If you prefer reusable listener objects, use `FunctionCallListener`.

This is useful when you want to collect tool-call lifecycle state rather than only respond inline through callbacks.

## Design note

The runtime-owned tool loop is an intentional architectural boundary.

It keeps tool execution behavior consistent across providers and avoids pushing orchestration logic down into provider adapters.

## See also

- `chat.md`
- `conversation-memory.md`
- `architecture.md`
- `getting-started.md`
