> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcat.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP: Laravel

> Track every laravel/mcp server of a Laravel application: install, the config/agentcat.php reference, hooks, Octane and tests.

The Laravel bridge tracks every [`laravel/mcp`](https://github.com/laravel/mcp) server of your application with no code: you install the package and set your project ID.

## Requirements

* PHP 8.3, 8.4 or 8.5.
* `laravel/mcp` 1.x, with Laravel 12.41.1 or later, or Laravel 13. On every pull request and every night, CI tests AgentCat with the newest patch of each `laravel/mcp` minor release from 1.0 up.
* ext-curl, to send the events.

## Install

```bash theme={null}
composer require agentcat/sdk
php artisan agentcat:install
```

Laravel discovers AgentCat's service provider. `agentcat:install` asks for your project ID, or takes it as an argument (`php artisan agentcat:install proj_…`):

* It checks the ID: `proj_` followed by 27 letters or digits.
* It writes `AGENTCAT_PROJECT_ID` into `.env`. When the application has no `.env` file, it creates one with that key only, and says so.
* It adds an empty `AGENTCAT_PROJECT_ID=` to `.env.example`, when the application has one.
* With `--config`, it also publishes `config/agentcat.php`.
* It sends nothing over the network.

Without the command, set `AGENTCAT_PROJECT_ID` in `.env` yourself. Without a project ID, no event goes to AgentCat; your [exporters](/integrations.md) still get them.

## What AgentCat tracks

* **Every `laravel/mcp` server** that the container resolves, also when `laravel/mcp` builds the server for each HTTP request. The server of [Laravel Boost](https://github.com/laravel/boost) is excluded by default.
* **Every tool call.** AgentCat adds a `session_id` and a `context` parameter to the input schema of each tool (and `agent_id` with `enable_agent_tracking`), and removes them from the arguments before your tool runs. It adds the `get_more_tools` tool.
* **After the response.** AgentCat runs your hooks and sends the events in Laravel's `terminating()` callbacks, after the client has its answer.

AgentCat reads the headers, method and URL of a call from the request of the server's own HTTP transport, not from `$_SERVER`, so Octane workers record them too.

## Configuration reference

To change the defaults, publish the configuration file:

```bash theme={null}
php artisan vendor:publish --tag=agentcat-config
```

Every key of `config/agentcat.php`:

| Key | Environment variable | Default | Accepted values |
| - | - | - | - |
| `enabled` | `AGENTCAT_ENABLED` | Unset: on, except while your tests run | `true`, `1`, `yes`, `on` or `false`, `0`, `no`, `off` (any case) |
| `project_id` | `AGENTCAT_PROJECT_ID` | Unset: no events go to AgentCat; exporters still run | `proj_` followed by 27 letters or digits |
| `exclude` | | `['Laravel\Boost\Mcp\Boost']` | A list of server classes; a string is one class |
| `projects` | | `[]` | Server class => project ID |
| `log_channel` | `AGENTCAT_LOG_CHANNEL` | Unset: `storage/logs/agentcat.log` | The name of a channel of `config/logging.php` |
| `api_url` | `AGENTCAT_API_URL`, then `MCPCAT_API_URL` | Unset: `https://api.agentcat.com` | A URL |
| `enable_report_missing` | | `true` | The words of `enabled` |
| `enable_tracing` | | `true` | The words of `enabled` |
| `enable_tool_call_context` | | `true` | The words of `enabled` |
| `custom_context_description` | | `null`: the default description | A string |
| `enable_agent_tracking` | | `false` | The words of `enabled` |
| `identify` | | `null` | The class name of an invokable class |
| `resolve_session_id` | | `null` | The class name of an invokable class |
| `event_tags` | | `null` | The class name of an invokable class |
| `event_properties` | | `null` | The class name of an invokable class |
| `redact_sensitive_information` | | `null` | The class name of an invokable class |
| `redact_event` | | `null` | The class name of an invokable class |
| `exporters` | | `null`: no exporters | Name => `['type' => 'otlp'`, `'datadog'`, `'sentry'` or `'posthog'`, then the exporter's fields]; a `null` entry is skipped |
| `disable_diagnostics` | `AGENTCAT_DISABLE_DIAGNOSTICS`, `DISABLE_DIAGNOSTICS` | Both unset: diagnostics on | `true`, `1`, `yes`, `on` turn diagnostics off; `false`, `0`, `no`, `off` and blank keep them on. In a list, one value that turns them off is enough. Any other value turns them off, with a warning. |
| `diagnostics_endpoint` | `AGENTCAT_DIAGNOSTICS_ENDPOINT` | Unset: `https://otel.agentcat.com` | The URL of an OpenTelemetry collector (OTLP/HTTP logs) |
| `diagnostics_token` | `AGENTCAT_DIAGNOSTICS_TOKEN` | Unset: AgentCat's | The bearer token of `diagnostics_endpoint` |
| `disable_signal_handlers` | | `false` | The words of `enabled` |

The file is safe with `php artisan config:cache`: the keys that hold environment variables read them through `env()`, and the service provider hands the configured values to AgentCat when the application boots.

A value of the wrong type, or a word that is not in the list, logs one warning line and AgentCat uses the default. When `enabled` holds a word that is not in the list, the `AGENTCAT_ENABLED` variable of the process decides, and without one, the default. An unknown key logs `WARN: Unknown AgentCat option "agentcat.<key>" ignored`.

### `exclude` and `projects`

`exclude` lists the server classes that AgentCat never tracks. A class also excludes its subclasses. An excluded server touches neither the AgentCat log nor diagnostics.

* **`null`, or no `exclude` key**, is the default list: Laravel Boost's server.
* **`[]`** excludes nothing, Laravel Boost's server included.
* **A blank string** names no server: AgentCat logs one warning and uses the default list.

`projects` sends the events of some servers to another project. The first class that the server is an instance of wins. A `null` project (`env()` of an unset variable) means `project_id`.

```php theme={null}
return [
    // ...
    'exclude' => [
        'Laravel\Boost\Mcp\Boost',
        App\Mcp\Servers\InternalServer::class,
    ],

    'projects' => [
        App\Mcp\Servers\BillingServer::class => env('AGENTCAT_BILLING_PROJECT_ID'),
    ],
];
```

### The log file

AgentCat writes its own lines to `storage/logs/agentcat.log`, through a Laravel `single` channel that it builds. They stay out of your application's log and its error trackers: `laravel/mcp` builds a server for each HTTP request, so the setup lines repeat on every request. To route them into one of your channels, set `log_channel`, for example to `stack`.

### Hooks are invokable classes

A hook in `config/agentcat.php` is the class name of an invokable class. A closure cannot go in this file, because `php artisan config:cache` cannot store it. AgentCat resolves the class from the container when the hook runs (under Octane, from the container of the request), and calls it with the same argument as the [PHP hook](/sdk/identifying-users.md): an `AgentCat\ToolCall` for `identify`, `resolve_session_id`, `event_tags` and `event_properties`, a string for `redact_sensitive_information`, and an `AgentCat\Event` for `redact_event`.

```php theme={null}
namespace App\AgentCat;

use AgentCat\Identity;
use AgentCat\ToolCall;

final class IdentifyActor
{
    public function __invoke(ToolCall $call): ?Identity
    {
        $token = $call->headers['authorization'] ?? null;

        return $token === null ? null : new Identity(userId: hash('sha256', $token));
    }
}
```

```php theme={null}
// config/agentcat.php
return [
    // ...
    'identify' => App\AgentCat\IdentifyActor::class,
];
```

A hook class that cannot be resolved, or that throws, logs one line when the hook runs. The tool call is never affected.

### Exporters

In `config/agentcat.php`, an exporter is an array with a `type` and the fields of its [integration page](/integrations.md), with the TypeScript field names:

```php theme={null}
return [
    // ...
    'exporters' => [
        'otlp' => ['type' => 'otlp', 'endpoint' => 'http://localhost:4318/v1/traces'],
        'datadog' => [
            'type' => 'datadog',
            'apiKey' => env('DD_API_KEY'),
            'site' => 'datadoghq.com',
            'service' => 'my-mcp-server',
        ],
        'sentry' => null, // a null entry is skipped
    ],
];
```

An entry with an unknown type, or a missing field, logs one line and is skipped; the other entries still work.

## Octane and other long-running workers

Under [Laravel Octane](https://laravel.com/docs/octane) (Swoole, RoadRunner or FrankenPHP), a worker serves many requests. After each request, AgentCat waits at most 1 second in `terminating()` for its delivery, so the worker is soon free for the next request.

* **The wait shows in request durations.** The worker's time for a request that sends events can be up to 1 second longer, in Octane's numbers and in your monitoring. The client has its answer before the wait.
* **An idle worker holds its last request's events.** Events that are still waiting after the 1 second go out at the next request of the worker, or when the worker stops.
* **When a worker stops**, AgentCat delivers what is left for at most 5 seconds, at Octane's `WorkerStopping` event. Measured on Swoole 6.2.3: when the whole server stops, Swoole did not cut that delivery. A worker that Swoole restarts (for example after Octane's `--max-requests`) is stopped by force about twice its `max_wait_time` server setting after it is told to stop: about 6 seconds with the default of 3. So a `max_wait_time` below 3 seconds can cut the delivery of a restarting worker short, and with the default, the other `WorkerStopping` listeners leave AgentCat its 5 seconds only when they take less than about 1 second.

Under PHP-FPM, FrankenPHP's classic mode and Apache, AgentCat does not wait in `terminating()`: it only starts the delivery there, and finishes it at the end of the script, after the response. See [PHP runtimes](/sdk/php/runtimes.md).

AgentCat binds one method object per tracked server in the container, under a key with the server's object ID. In a worker, the binding of a server that is gone stays until PHP reuses its object ID or the container ends.

## Your own method handlers

A `tools/list`, `tools/call` or `initialize` class that your server declares in its `$methods` property is wrapped: AgentCat delegates to it and records one event per call. For your own `tools/call` class, the event of a failing tool carries the error of the answer, not the original exception.

A method handler that you set in the server's `boot()` method replaces AgentCat's for that server: `laravel/mcp` calls `boot()` after AgentCat has tracked the server. AgentCat then records no event for that method, and logs nothing:

```php theme={null}
namespace App\Mcp\Servers;

use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Methods\CallTool;

class AuditedCallTool extends CallTool
{
    // Your own tools/call handling.
}

final class BillingServer extends Server
{
    protected function boot(): void
    {
        // AgentCat never sees this handler: no event for any tools/call of this server.
        $this->addMethod('tools/call', AuditedCallTool::class);
    }
}
```

Declare such a handler in the `$methods` property instead.

## ToolSearch catalogs

A server that exposes its tools through a `laravel/mcp` ToolSearch catalog has the two tools `search_tools` and `execute_tools`. AgentCat injects its parameters into them like into any other tool. One `execute_tools` call is one event: AgentCat does not record the tool calls that run inside it one by one.

## Streamed answers

A tool whose `handle()` is a generator answers with a stream. Its notifications pass unchanged, and AgentCat makes the event from the final result, so the event's duration includes the whole stream. A stream that the client abandons publishes no event.

## Client name and version

`laravel/mcp` 1.x has no MCP sessions. AgentCat takes the client's name and version from the `_meta` of the call, else from the `initialize` request of an older client to the same server instance. A stdio server keeps that for the whole process. Over HTTP it is lost, because each request builds a new server. The events have no transport session ID and no auth object: the `Authorization` header is in the request headers.

## Responses on Apache mod\_php, php-cgi and `php -S`

`laravel/mcp` sends its JSON answers without a `Content-Length` header. Where PHP sends a response only when the script ends, the bridge gives the plain JSON answers of a tracked server their `Content-Length`, so the client has the whole answer before AgentCat delivers. [PHP runtimes](/sdk/php/runtimes.md) has the rules and the runtimes where it does that.

## Testing

AgentCat is off while your tests run (PHPUnit, Pest, or `APP_ENV=testing`): your servers answer as without AgentCat, and `laravel/mcp`'s test helpers see their own answers. To assert on the events of a test, call `AgentCat::fake()` before the test builds a server, and restore the real client in `tearDown()`:

```php theme={null}
namespace Tests\Feature;

use AgentCat\AgentCat;
use AgentCat\Event;
use App\Mcp\Servers\WeatherServer;
use App\Mcp\Tools\CurrentWeatherTool;
use Tests\TestCase;

final class WeatherToolTest extends TestCase
{
    protected function tearDown(): void
    {
        AgentCat::setClient(null);
        parent::tearDown();
    }

    public function test_the_weather_tool_call_is_tracked(): void
    {
        $fake = AgentCat::fake();

        WeatherServer::tool(CurrentWeatherTool::class, ['location' => 'Paris'])->assertHasNoErrors();

        $fake->assertTrackedCount(1);
        $fake->assertTracked(fn (Event $event): bool => $event->isError !== true);
    }
}
```

The [`mcp/sdk` page](/sdk/php/mcp-sdk#testing) says what each assertion of the fake counts. Three things to know in a Laravel suite:

* **Two settings outlive the test that made them.** The fake stays the shared client of the PHP process until `AgentCat::setClient(null)`. The `agentcat.enabled` value that the service provider reads when the application boots stays in effect until the next application boots.
* **With the fake, AgentCat changes the answers as in production.** A call without a `session_id` gets the issued session ID as the first text block of `content`. A tool with an output schema gets an `mcp_session` entry in its `structuredContent`. An assertion on the exact `structuredContent` (for example `assertStructuredContent()`) then sees that entry.
* **A bad value in `config/agentcat.php` logs one line per test application**, also without the fake, because each test boots a new application. The lines go to `storage/logs/agentcat.log`.

## Laravel Boost guidelines

The package ships guidelines for AI coding agents in `resources/boost/guidelines/core.blade.php`. When you run `php artisan boost:install`, [Laravel Boost](https://github.com/laravel/boost) adds them to your agent's guidelines, so your coding agent knows how AgentCat is set up: the install command, the configuration keys, the hooks as invokable classes, and `AgentCat::fake()` in tests.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.