> ## 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 reference

> Options, frameworks, runtimes, delivery, logging, testing, and limits of the AgentCat PHP SDK.

This page describes the `agentcat/sdk` Composer package, version 1.x. For the first setup, see [PHP setup](https://docs.agentcat.com/languages/php.md).

## Options

`AgentCat::track(object $server, ?string $projectId = null, ?Options $options = null)` tracks an `mcp/sdk` server, as the `Mcp\Server\Builder` or the `Mcp\Server` built from it. A `null` project ID reads `AGENTCAT_PROJECT_ID`, and `''` sends events only to your exporters. `AgentCat\Options` takes named arguments, all optional.

The Laravel bridge reads the same options from `config/agentcat.php`, and the Symfony bundle from its `agentcat` key, with snake\_case names. There, a hook is the name of an invokable class (Laravel) or service (Symfony), because a cached configuration cannot store a closure.

| Option | Laravel and Symfony key | Default | What it does |
| - | - | - | - |
| `identify` | `identify` | none | `(ToolCall) => ?Identity`. Names the user of each tool call. See [Identifying users](https://docs.agentcat.com/languages/php.md#identifying-users). |
| `enableToolCallContext` | `enable_tool_call_context` | `true` | Adds the `context` parameter, where the agent explains why it calls the tool: the source of intent data. |
| `customContextDescription` | `custom_context_description` | built-in text | Replaces the description of the `context` parameter. |
| `enableReportMissing` | `enable_report_missing` | `true` | Adds the `get_more_tools` tool, so agents can report tools that they did not find. |
| `enableTracing` | `enable_tracing` | `true` | Publishes one event for each tool call and injects the `session_id` parameter. |
| `enableAgentTracking` | `enable_agent_tracking` | `false` | Adds an `agent_id` parameter, sent as the `agentcat_agent_id` tag. Off by default, because a strict client can refuse calls without it. |
| `resolveSessionId` | `resolve_session_id` | none | `(ToolCall) => ?string`. Your own session ID for each call. `null` lets AgentCat choose. |
| `redactSensitiveInformation` | `redact_sensitive_information` | none | `(string) => string`. Changes every string of an event before it leaves your server. |
| `redactEvent` | `redact_event` | none | `(Event) => ?Event`. Returns a changed event, or `null` to drop it. |
| `eventTags` | `event_tags` | none | `(ToolCall) => ?array`. String tags that you can filter on. See [Tags and properties](https://docs.agentcat.com/sdk/event-tags-properties.md). |
| `eventProperties` | `event_properties` | none | `(ToolCall) => mixed`. JSON data for each event. |
| `exporters` | `exporters` | none | `OtlpExporter`, `DatadogExporter`, `SentryExporter`, or `PostHogExporter`, keyed by a name that you choose. See [Integrations](https://docs.agentcat.com/integrations.md). |
| `apiBaseUrl` | `api_url` | `https://api.agentcat.com` | The address that receives events. If not set, the SDK reads `AGENTCAT_API_URL`, then `MCPCAT_API_URL`. |
| `disableDiagnostics` | `disable_diagnostics` | `false` | Stops [diagnostics](#diagnostics). |
| `disableSignalHandlers` | `disable_signal_handlers` | `false` | Stops the SIGTERM and SIGINT handlers in a CLI server, so a signal ends the process without the last delivery. |

> [!NOTE]
>
> We recommend that you keep the defaults of `enableToolCallContext`, `enableReportMissing`, `enableTracing`, and `disableDiagnostics`, because each one feeds a part of the dashboard or helps us fix a broken install.

`exporters`, `disableDiagnostics`, and `disableSignalHandlers` apply to the whole process. The first `track()` call decides them.

`Identity` has `userId`, `userName`, and `userData`. `ToolCall` has `toolName`, `arguments`, `meta`, `headers` (lower-case names, empty on stdio), the client and protocol versions, `transportSessionId`, and the `request` and `context` of your MCP library.

| Variable | What it does |
| - | - |
| `AGENTCAT_PROJECT_ID` | The project ID. |
| `AGENTCAT_ENABLED` | `true` turns AgentCat on everywhere, tests included, and `false` turns it off. Unset: on, except in tests. |
| `AGENTCAT_API_URL` | The event address, when `apiBaseUrl` is not set. `MCPCAT_API_URL` is the old name. |
| `AGENTCAT_DISABLE_DIAGNOSTICS` | `true` stops diagnostics. `DISABLE_DIAGNOSTICS` can only stop them, and off wins. |
| `AGENTCAT_DIAGNOSTICS_ENDPOINT`, `AGENTCAT_DIAGNOSTICS_TOKEN` | Send diagnostics to your own OTLP/HTTP collector. |
| `AGENTCAT_LOG_CHANNEL` | Laravel only: the log channel for AgentCat. |

## Framework and transport details

The SDK needs ext-curl, ext-json, and ext-mbstring, and we recommend ext-pcntl for stdio servers. CI tests `mcp/sdk` 0.8.1 and later, `laravel/mcp` 1.x, and `symfony/mcp-bundle` 0.13 and 0.14, on PHP 8.3 to 8.5.

### Official PHP MCP SDK

* `track()` can come before or after your tools and `build()`. It never throws: on failure, it logs the reason and returns the server unchanged. A second call changes nothing.
* After `build()`, a call to an unknown tool, a call with invalid arguments, and a call that your own `tools/call` handler answers give no event. A server with `setReferenceHandler()` stays untracked. Tracking the Builder before `build()` avoids both.
* Your own `tools/call` and `tools/list` handlers keep working, because AgentCat's handlers delegate to them.

#### HTTP servers

This script answers one request under PHP-FPM, FrankenPHP, or Apache. It needs a PSR-17 implementation such as `nyholm/psr7`, and a session store that all requests share, such as `FileSessionStore`:

```php theme={null}
<?php

declare(strict_types=1);

use AgentCat\AgentCat;
use Http\Discovery\Psr17Factory;
use Mcp\Server;
use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport;

require __DIR__ . '/vendor/autoload.php';

$builder = Server::builder()
    ->setServerInfo('my-server', '1.0.0')
    ->setSession(new FileSessionStore(__DIR__ . '/sessions'))
    ->addTool(static fn(int $a, int $b): int => $a + $b, 'add', description: 'Adds two numbers');

AgentCat::track($builder);

$factory = new Psr17Factory();
$request = $factory->createServerRequestFromGlobals();
$response = $builder->build()->run(new StreamableHttpTransport($request, $factory, $factory));

$body = (string) $response->getBody();
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
    foreach ($values as $value) {
        header($name . ': ' . $value, false);
    }
}
// Apache mod_php, php-cgi, and php -S: with the length, the client has the whole answer before AgentCat delivers.
header('Content-Length: ' . strlen($body));
echo $body;
```

AgentCat reads the request of a plain `mcp/sdk` call from `$_SERVER`, so a long-running CLI HTTP server, such as ReactPHP or AMPHP, sends events with no headers.

#### Stateless endpoints

`Builder::buildStateless()` returns a `StatelessProtocol`, not an `Mcp\Server`, so you track the Builder. `publishCustomEvent()` also takes the Builder, and throws with the `StatelessProtocol`.

#### Custom events

`AgentCat::publishCustomEvent($builder, $projectId, new CustomEvent(...))` queues a [custom event](https://docs.agentcat.com/sdk/custom-events.md) and returns nothing. It throws `InvalidArgumentException` when the project ID is `null` or empty, also for a tracked server. A session ID string as the first argument sends the event into that session, with no hooks. A `\Throwable` as `error` arrives as `{}`, so we recommend an array with the message and class.

### Laravel

The bridge tracks every `laravel/mcp` server that the container resolves, also one built per request. `php artisan agentcat:install` checks the project ID, writes it into `.env`, and with `--config` publishes `config/agentcat.php`. Besides the options, that file has `enabled`, `project_id`, `exclude` (server classes, by default the [Laravel Boost](https://github.com/laravel/boost) server), `projects` (server class => project ID), `log_channel`, and the diagnostics endpoint and token. A bad value or unknown key logs a warning and uses the default.

* A hook class resolves from the container when it runs. If it fails, AgentCat logs a line, and the tool call is not affected.
* `laravel/mcp` 1.x has no MCP sessions. Over HTTP, the client name comes only from the `_meta` of each call.
* A handler in the `$methods` property of your server keeps its events. A handler that you set in `boot()` replaces AgentCat's handler, so that method gives no event.

### Symfony

The bundle tracks every server of `mcp.servers`. With Flex contrib recipes, the recipe registers the bundle. Besides the options, the bundle has `enabled`, `project_id`, `exclude_servers`, `projects` (server name => project ID), `logger`, and the diagnostics endpoint and token. `config:dump-reference agentcat` lists them.

> [!WARNING]
>
> We recommend `%env(default::NAME)%` for every AgentCat value from the environment. A bare `%env(NAME)%` with an unset variable makes the whole `agentcat` configuration unreadable, and AgentCat tracks no server.

A hook is the ID of an invokable service. Hooks run after the response, when the `RequestStack` is empty, so a hook reads `$call->headers` instead.

## Runtimes, delivery, and shutdown

AgentCat runs hooks and sends events after the client has its answer, where the runtime allows it. Up to 5 sends run at once. A failed send gets 3 more tries, after 1, 2, and 4 seconds. The queue holds 10,000 events, and when it is full, AgentCat drops the oldest. At the end of a script or process, AgentCat has 5 seconds for waiting hooks and events, and the rest are lost. PHP cannot interrupt a running hook, so we recommend fast hooks.

| Runtime | When AgentCat delivers | What you do |
| - | - | - |
| PHP-FPM, NGINX Unit, FrankenPHP classic mode | After the response, at the end of the script | Nothing |
| FrankenPHP worker mode, Swoole, OpenSwoole, RoadRunner, Workerman | Octane and Symfony Runtime: after each request, waiting at most 1 second | A raw worker loop calls `AgentCat::flush(1.0)` after each request |
| Apache mod\_php, php-cgi, `php -S` | At the end of the script, after the client has a response with a `Content-Length` | A plain `mcp/sdk` script sets `Content-Length`. The bridges set it for you |
| The CLI (stdio) | At the next `tools/call` or `tools/list`, at the end of input, and at SIGTERM or SIGINT | Install ext-pcntl. In a container, use `docker run --init` |

* **PHP-FPM** keeps the worker busy during delivery. FPM stops a worker at `request_terminate_timeout`, so we recommend at least 6 seconds plus your hook time over your slowest request.
* **Octane** keeps the events of an idle worker until its next request or until it stops. A Swoole `max_wait_time` below 3 seconds can cut the final delivery short.
* **Signals.** Without ext-pcntl (on Windows, and in the official PHP Docker images until `docker-php-ext-install pcntl`), events that wait at a signal are lost. Under Swoole and OpenSwoole, AgentCat installs no signal handlers.
* **Async runtimes** that interleave calls in one process, such as Swoole coroutines or ReactPHP, are not supported in version 1.
* **Hardened hosts.** Without the ext-curl functions in `disable_functions`, AgentCat sends nothing, and it logs each lost feature.

`AgentCat::flush(?float $timeoutSeconds = null): bool` sends waiting events and returns `false` on timeout. `AgentCat::shutdown()` stops accepting events and sends the waiting ones for at most 5 seconds. Most servers need neither.

## Logging and debug mode

The SDK has no debug option. It always writes its lines to a log, never to STDOUT or STDERR: `~/agentcat.log` for `mcp/sdk` (or the PSR-3 logger of the first Builder that has one), `storage/logs/agentcat.log` in Laravel (or `log_channel`), and `%kernel.logs_dir%/agentcat.log` in Symfony (or the `logger` service).

Useful log lines:

* `AgentCat setup complete | project ... | tracing=true context=true reportMissing=true exporters=0`: `track()` worked.
* `Successfully sent event ... | mcp:tools/call | session ... | <user>`: an event reached AgentCat, for the user ID from `identify`, or `anonymous`.
* `Shutting down with <n> events still in queue`: the 5-second budget ran out.

## Testing

AgentCat does nothing under PHPUnit or Pest, or when `APP_ENV` is `testing` or `test`. `AGENTCAT_ENABLED=true` turns it on in tests. To assert on events, call `AgentCat::fake()` before the test tracks its server:

```php theme={null}
<?php

declare(strict_types=1);

use AgentCat\AgentCat;
use AgentCat\Event;
use Mcp\Server;
use Mcp\Server\Transport\InMemoryTransport;
use PHPUnit\Framework\TestCase;

final class EchoToolTest extends TestCase
{
    protected function tearDown(): void
    {
        AgentCat::setClient(null); // the real client again, which stays off in tests
    }

    public function testTheEchoCallIsTracked(): void
    {
        $fake = AgentCat::fake(); // first: a server tracked before fake() is not tracked at all

        $builder = Server::builder()
            ->setServerInfo('echo-mcp', '0.1.0')
            ->addTool(static fn(string $msg): string => $msg, 'echo', description: 'Echo a message');
        AgentCat::track($builder, 'proj_YOUR_PROJECT_ID');

        $builder->build()->run(new InMemoryTransport([
            '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}',
            '{"jsonrpc":"2.0","method":"notifications/initialized"}',
            '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hi"}}}',
        ]));

        $fake->assertTrackedCount(1);
        $fake->assertTracked(static fn(Event $event): bool => $event->resourceName === 'echo');
    }
}
```

The `laravel/mcp` test helpers work the same way. The fake also has `assertNothingTracked()`, `assertCustomEventPublished()`, `events()`, and `logs()`. It changes answers as in production, so the first call gets a session ID text block, and exact schema assertions see `session_id` and `context`.

## Differences and known limits

The PHP SDK matches the TypeScript SDK, with these differences:

* **Hooks run after the response, with no time limit.** The TypeScript SDK runs them during the call, with a 30-second limit.
* **A `null` project ID** reads `AGENTCAT_PROJECT_ID`. In TypeScript, it means no project.
* **Every page of `tools/list` is read** under PHP-FPM, so your own `tools/list` handler runs once for each page.
* **A `context` that is a number, boolean, or object** drops the event.
* **Delivery** uses ext-curl and the libcurl proxy settings (`HTTPS_PROXY`, `NO_PROXY`), and follows no redirects.
* **Exporters** can get up to 4 copies of an event.

### Request headers

An HTTP event carries every request header, `Authorization` and `Cookie` included, under `parameters.extra.requestInfo.headers`. On `mcp/sdk` 0.9 and later, it also carries OAuth token scopes and claims under `parameters.extra.authInfo`. A `redactEvent` hook can remove them.

### Diagnostics

AgentCat sends its own log lines to `https://otel.agentcat.com` to find broken installs. They can contain session IDs, user IDs, tool names, errors, your project ID, and `APP_ENV`. Diagnostics are off in tests, and `disableDiagnostics` or `AGENTCAT_DISABLE_DIAGNOSTICS=true` turns them off.

### Known limits

* **Sizes.** AgentCat cuts each string to 32 KB, each event to 100 KB, and the intent text to 2,048 characters. Objects keep 10 levels and 100 keys or items for each level.
* **Long names.** The AgentCat API drops events with a name over 255 characters or a version over 50.
* **Your own shutdown functions** that call `exit()` lose the waiting events.
* **Forks** after the first event can send waiting events twice.
* **`symfony/mcp-bundle` 0.15** is not in the test matrix yet, and a new install picks it. You can stay on the tested range with `composer require symfony/mcp-bundle:^0.14`.

The [PHP SDK README](https://github.com/agentcathq/agentcat-php-sdk#readme) has the full runtime notes, including hardened hosts, signal handling, and the FrankenPHP worker loop.


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