> ## 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: differences and limits

> Where the PHP SDK differs from the TypeScript SDK, what it sends, and its known limits.

The PHP SDK follows the TypeScript SDK: the same events, the same injected parameters, and the same log lines where both SDKs log the same thing. This page lists where it differs, what it sends besides your events, and its known limits.

## Supported versions

The Requirements of each guide list the supported versions: [`mcp/sdk`](/sdk/php/mcp-sdk#requirements), [Laravel](/sdk/php/laravel.md#requirements) and [Symfony](/sdk/php/symfony.md#requirements). PHP 8.6 is not supported yet.

## Differences from the TypeScript SDK

### Every page of `tools/list` is read

When a `tools/call` arrives before a `tools/list` (under PHP-FPM, that is every call), AgentCat lists the server's tools again to learn their schemas. The TypeScript SDK reads only the first page there, so a tool on a later page falls back to a guess that can remove the tool's own `session_id` or `context` argument. The PHP SDK reads the pages until it finds the called tool. It stops at the last page, at a cursor it has seen before, after 100 pages, or after 5 seconds. It lists `get_more_tools` once, on the last page.

There is one exception: a server that registers its own `get_more_tools` on a page before the last. AgentCat answers the calls to that tool. A process that has not read your tool's page cannot know your tool, so it lists AgentCat's `get_more_tools` on the last page too, and a client sees two tools of that name. Under PHP-FPM and the other per-request runtimes, that is every request. A long-running process that has read your tool's page lists only yours. Rename your tool: AgentCat logs a warning when it finds one.

`laravel/mcp` puts 15 tools on a page, so this matters for every Laravel server with 16 or more tools. It costs time only for a tool on a later page. Under PHP-FPM, a call to the last-page tool of a `laravel/mcp` server with 300 tools takes 57.6 ms instead of 2.7 ms; a call to a tool on the first page costs the same as before. A `tools/list` handler of your own (one that you add to the server, or a `laravel/mcp` class that answers `tools/list`) runs once for each page that AgentCat reads during a `tools/call` under PHP-FPM, so its queries, metrics and log lines repeat that many times.

### Hooks run after the response, with no time limit

The TypeScript SDK runs your hooks (`identify`, `resolveSessionId`, `eventTags`, `eventProperties`) while your tool runs, and leaves out a hook that does not finish within 30 seconds. The PHP SDK runs them after the client has its answer, so they never add latency. Synchronous PHP cannot interrupt a hook: there is no 30-second limit, and the event waits until each hook returns. In the same way, the new listing of the tools (above) checks its 5 seconds between pages, but it cannot interrupt a `tools/list` handler that hangs. A slow hook makes the end of a request or a process slower: see [the 5-second budget](/sdk/php/runtimes.md).

On a stdio server, the hooks of a call run when the next call starts, so a slow hook delays the next call.

### A `context` that is not a string

When an agent sends a number, a boolean or an object as the `context` argument, AgentCat drops the event before any hook runs, and logs `Failed to truncate event: UnexpectedValueException: userIntent is not a string (<type>)`. The TypeScript SDK runs the hooks first, then drops the event. A list as `context` is sent with no intent; the TypeScript SDK sends the list.

### Errors in custom events

The `error` of a custom event is sent as you pass it. A `\Throwable` arrives as an empty object, `{}`, as an `Error` does in the TypeScript SDK. Pass the details you want:

```php theme={null}
use AgentCat\AgentCat;
use AgentCat\CustomEvent;
use App\Billing\Payments;

try {
    Payments::chargeOpenInvoices();
} catch (\RuntimeException $e) {
    AgentCat::publishCustomEvent($builder, 'proj_YOUR_PROJECT_ID', new CustomEvent(
        resourceName: 'payment_failed',
        isError: true,
        error: ['message' => $e->getMessage(), 'type' => $e::class],
    ));
}
```

A first argument of `publishCustomEvent()` that is neither an object nor a string is a PHP `TypeError` (with `declare(strict_types=1)`), not the TypeScript SDK's error message.

### The project ID

`AgentCat::track($server, null)` reads the project ID from `AGENTCAT_PROJECT_ID`. `''` sends the events to no AgentCat project, only to your exporters. In the TypeScript SDK, `null` means no project.

### The client of a per-request server

Under PHP-FPM and the other per-request runtimes, an `mcp/sdk` server with a session store keeps the client name and version of the `initialize` handshake for the later calls of that session. The TypeScript SDK reports none there.

### Tools that ask for more input

A tool that asks the client for more input is called again for each round, and each round is its own event, with both versions of the protocol. On the handshake protocol, the TypeScript SDK records only the final result.

### Additions of the PHP SDK

* `Options(disableSignalHandlers: true)` turns off AgentCat's SIGTERM and SIGINT handlers. The TypeScript SDK has no such switch.
* `AgentCat::flush()` and `AgentCat::shutdown()` deliver on demand. They are rarely needed.
* `AgentCat::fake()` records the events of your tests.
* AgentCat sends through ext-curl. It uses the proxy settings that libcurl reads (`HTTPS_PROXY`, `NO_PROXY`), and it does not follow redirects.

## What the PHP SDK sends

### Request headers

The event of a call over HTTP carries every request header of that call, verbatim, `Authorization` and `Cookie` included, under `parameters.extra.requestInfo.headers`. The TypeScript SDK does the same. On `mcp/sdk` 0.9 and later, when the server checked an OAuth access token for the request, the event also carries that token's scopes and claims, under `parameters.extra.authInfo`. To keep a header or the token's details out of your events, remove them in a `redactEvent` hook:

```php theme={null}
use AgentCat\AgentCat;
use AgentCat\Event;
use AgentCat\Options;

AgentCat::track($builder, 'proj_YOUR_PROJECT_ID', new Options(
    redactEvent: static function (Event $event): Event {
        $headers = $event->parameters->extra->requestInfo->headers ?? null;
        if ($headers instanceof \stdClass) {
            unset($headers->authorization, $headers->cookie);
        }
        $extra = $event->parameters->extra ?? null;
        if ($extra instanceof \stdClass) {
            unset($extra->authInfo);
        }

        return $event;
    },
));
```

### Diagnostics

To find broken installs, AgentCat sends its own log lines to AgentCat (`https://otel.agentcat.com`): every line it logs, with its time and level, also when a PSR-3 logger or a log channel of yours gets the lines instead of `agentcat.log`. These lines can contain:

* session IDs and actor IDs: AgentCat logs one line for each event it sends, in the form `Successfully sent event <event ID> | <event type> | session <session ID> | <project ID> | <duration> ms | <actor ID>`;
* tool names, and error messages, which hold whatever the code that threw put in them;
* the answers of exporters that failed, quoted.

Each batch also carries your project ID (without one, an install ID: a hash of the host name and the install path), the OS type and version, the architecture, the versions of PHP, of the SDK and of `mcp/sdk`, the process ID and `APP_ENV`. The lines that AgentCat logs at the very end of a request or process, after it has posted its last diagnostics, never reach diagnostics: for example the `Successfully sent event` lines of the events that this last pass sends, the outcomes of the exports in it, and `Shutting down with <n> events still in queue`.

Diagnostics are on by default, and off while your tests run. To turn them off:

* `new Options(disableDiagnostics: true)` on the first `track()` of the process, or `disable_diagnostics` in the Laravel or Symfony configuration;
* or the environment variable `AGENTCAT_DISABLE_DIAGNOSTICS=true`. The TypeScript name `DISABLE_DIAGNOSTICS=true` works too, but only to turn them off; when the two disagree, off wins.

`AGENTCAT_DIAGNOSTICS_ENDPOINT` and `AGENTCAT_DIAGNOSTICS_TOKEN` send them to your own OpenTelemetry collector instead. The PHP SDK does not read the unprefixed `DIAGNOSTICS_ENDPOINT` and `DIAGNOSTICS_TOKEN` of the TypeScript SDK, so your application's own variables of those names never reach AgentCat.

## Exporters

The exporters behave as in the TypeScript SDK:

* **Retries.** When the AgentCat API fails and AgentCat retries, it sends the event to your exporters again on each attempt, so an exporter can get up to 4 copies.
* **OTLP and fractional durations.** An OTLP export of an event whose duration is not a whole number (a custom event with `duration: 1.5`, for example) sends nothing and logs `Telemetry export failed for <name>: OTLP export error: RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer`.
* **Sentry.** An event's timestamp is the end of its transaction, which starts `duration` earlier. `environment` and `release` are tags of transactions and error events.
* **PostHog** gets the `parameters` and the `response` of each call.
* **One set per process.** The first `track()` of a process that passes exporters sets them up for every server. A later `track()` with other exporters logs `WARN: AgentCat exporters were set up by an earlier track(); the exporters of this call are ignored`.

## Known limits

* **Session IDs.** AgentCat accepts a `session_id` that has the shape of an AgentCat session ID. It does not check that AgentCat issued it.
* **Your own shutdown functions.** AgentCat delivers in a shutdown function that runs after yours. A shutdown function of yours that calls `exit()` ends the shutdown before that: the waiting events are lost.
* **Long names.** AgentCat cuts tool names, server names and versions at 256 characters plus `...`, as the TypeScript SDK does. The AgentCat API keeps at most 255 characters for a name and 50 for a version, so it drops such events.
* **Forks.** A process that forks after its first event copies the waiting events into the child, so they can be sent twice.
* **Runtimes.** [PHP runtimes](/sdk/php/runtimes.md) lists the limits of each runtime: sessions and time limits under PHP-FPM, responses without `Content-Length` on Apache, signals in containers, async runtimes, and hardened hosts.


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