Skip to main content
AgentCat runs your hooks and sends the events after the client has its answer, wherever the runtime allows it. This page says how each PHP runtime does it, and what you must do, if anything. There are two exceptions: on Apache mod_php, php-cgi and php -S, a client whose answer has no Content-Length waits for the delivery (see the Apache section), and a stdio server delivers the events of a call when the next call arrives, so the client of that next call waits for it (see the CLI section).

Overview

AgentCat needs ext-curl, ext-json and ext-mbstring. ext-pcntl is optional and recommended for CLI servers.

The 5-second budget

When a script or a process ends, AgentCat runs the hooks that still wait and sends the waiting events, for at most 5 seconds in all. Events that are still waiting when the time is up are lost, and AgentCat logs Shutting down with <n> events still in queue. AgentCat cannot stop a hook that runs: synchronous PHP cannot interrupt it. A slow hook, or a hook that waits for a slow service, therefore makes the end of a request or a process last longer than 5 seconds. Keep hooks fast.

PHP-FPM

fastcgi_finish_request() sends the response to the client. Then, in its shutdown function, AgentCat runs the hooks and sends the events. The FPM worker stays busy during that time, up to the 5-second budget: count it when you size the pool.
  • The session lock. On every web runtime, before it delivers, AgentCat writes and closes an active PHP session (session_write_close()), so the next request of the same client does not wait for the lock. A destructor that writes to $_SESSION after that loses its write.
  • The time limit. On every web runtime, when max_execution_time is set, AgentCat gives its delivery its own limit with set_time_limit(6), so a request that used most of its time does not end the delivery with a fatal error. That call can also shorten a longer limit for code that runs after AgentCat’s shutdown function.
  • request_terminate_timeout. This limit of the FPM pool counts wall-clock time, and PHP code cannot change it. When it is shorter than your slowest request plus the delivery, FPM kills the worker during the delivery, and the events of that request are lost. Leave at least 6 seconds plus the time your hooks take over your slowest request.
  • Diagnostics. AgentCat sends its diagnostics after the response, within the budget. Each diagnostics request has a 250 ms connect timeout and a 1-second total timeout, so a collector that does not answer holds the worker only briefly.
  • A host that disables fastcgi_finish_request() gets the behavior of Apache mod_php (below): the response goes out at the end of the script.

FrankenPHP

Classic mode works like PHP-FPM, with frankenphp_finish_request(). Worker mode keeps the script alive between requests, so shutdown functions do not run after each request. Laravel Octane and Symfony Runtime need nothing: AgentCat delivers in their terminate hooks and waits at most 1 second after each request. A raw frankenphp_handle_request() loop must call AgentCat::flush() after each request:
Copy the while loop exactly: the AgentCat::flush(1.0) call after frankenphp_handle_request() is what delivers each request’s events. Events that are still waiting after the 1 second go out after a later request, or when the worker stops.
  • Worker detection. AgentCat detects worker mode from the FRANKENPHP_WORKER environment variable that FrankenPHP sets. Inside a request, $_SERVER does not have it, so keep the variable in the worker’s environment.
  • Errors after the response. The official FrankenPHP Docker image has no php.ini, so log_errors is off, and a fatal error after the response (in a hook, for example) leaves no trace. Turn it on in the Caddyfile, or ship a php.ini:

Apache mod_php, php-cgi and php -S

These runtimes have no function that sends the response before the script ends. So at the start of its last shutdown pass, AgentCat sends and closes the output buffers that can be closed, and flushes them. A client that knows the length of the answer (Content-Length) then has all of it before AgentCat runs the hooks and delivers. A client of an answer without a length waits until the script ends, up to the 5-second budget.
  • The Laravel and Symfony bridges set Content-Length on the plain JSON answers of a tracked server. They never set it on a streamed (SSE) answer, on an answer without a body (1xx, 204, 304) or to a HEAD request, on an answer that has a Content-Length or Transfer-Encoding already, or after other output went out first.
  • A plain mcp/sdk script sets it itself, before it echoes the answer: header('Content-Length: ' . strlen($body)); (see the HTTP server of the mcp/sdk page).
  • Headers after that point. A destructor that sets a header after AgentCat closed the buffers gets a “headers already sent” warning.
  • zlib.output_compression. AgentCat closes the compression buffer with the others. Output written after that, for example an echo in a destructor, is not part of the compressed body and breaks it.
  • Warnings. Under Laravel and Symfony, the framework’s error handler catches PHP’s warnings, so they reach the framework’s log, not Apache’s error log.

NGINX Unit

NGINX Unit is supported like PHP-FPM, and you need nothing. Its PHP module reports the SAPI of php -S (cli-server); AgentCat recognizes Unit by its SERVER_SOFTWARE (Unit/<version>) and sends the response with Unit’s fastcgi_finish_request() before it delivers.

The CLI and containers

A stdio server has no request end. AgentCat runs the hooks and sends the events of a call when the next tools/call or tools/list request arrives, and when the input ends (see differences and limits for what that means for slow hooks). AgentCat writes nothing to STDOUT or STDERR: STDOUT stays the JSON-RPC stream.
  • Signals. With ext-pcntl, AgentCat sends the waiting events at SIGTERM and SIGINT. The mcp/sdk page explains the handlers, inherited ignores and disableSignalHandlers. The official PHP Docker images have no ext-pcntl: add RUN docker-php-ext-install pcntl to your image.
  • PID 1. In a container, the process with PID 1 ignores a signal whose handler is the default one. After its delivery, AgentCat gives the signal back to that default handler, which then does not end the process. Start the server with docker run --init (or init: true in Docker Compose), as production images should, so that an init process is PID 1.
  • Thread-safe (ZTS) builds, such as the official php:*-zts images: a second SIGTERM or Ctrl+C during the delivery waits until the delivery ends, up to the 5-second budget.
  • Windows has no ext-pcntl, so the events that wait at Ctrl+C are lost. AgentCat logs no line for that.
  • Symfony’s bin/console mcp:server ends with exit code 0 after the delivery at SIGTERM, because Symfony Console handles the signal itself, not 143.

Swoole, OpenSwoole, RoadRunner and Workerman

  • With Laravel Octane or Symfony Runtime, AgentCat delivers in the framework’s terminate hooks: Laravel’s terminating() callbacks and Symfony’s kernel.terminate. It waits at most 1 second after each request. Events that are still waiting go out at a later request of the worker, or when the worker stops. See Laravel for Octane’s worker stop.
  • A raw worker loop calls AgentCat::flush(1.0) after each request, as in the FrankenPHP loop.
  • No signal handlers under Swoole and OpenSwoole. When ext-swoole or ext-openswoole is loaded, AgentCat installs no SIGTERM or SIGINT handlers: those servers stop their workers with handlers that PHP cannot see. The check looks at the loaded extension, so a stdio server on a PHP with ext-swoole loaded gets no delivery at those signals either.
  • RoadRunner and Workerman install PHP signal handlers. AgentCat keeps them: after its delivery, it calls the handler that was there before.
  • No async runtimes. Version 1 does not support async runtimes that interleave calls in one process, such as Swoole coroutines or ReactPHP. Serve one request at a time per worker.

Hardened hosts

Shared hosts often list functions in disable_functions. AgentCat keeps your server answering whatever they disable. Where it loses a feature, it logs one line. “Once per process” means once per request under PHP-FPM, mod_php and php-cgi, because PHP keeps no state between their requests. A failure that repeats on every pass of one delivery is logged once, with (<n> more times in this delivery) after it. What AgentCat needs:
  • ext-curl, with the curl_multi_* functions, to send anything.
  • One of hrtime() and microtime(). With both disabled, delivery fails.
  • PHP’s core string, array, math, JSON, PCRE, mbstring, hash and error-handler functions, like every Composer package. No hosting list disables them.