Skip to main content
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, Laravel and Symfony. 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. 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:
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:

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 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.