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 logsShutting 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$_SESSIONafter that loses its write. - The time limit. On every web runtime, when
max_execution_timeis set, AgentCat gives its delivery its own limit withset_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, withfrankenphp_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:
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_WORKERenvironment variable that FrankenPHP sets. Inside a request,$_SERVERdoes 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, solog_errorsis off, and a fatal error after the response (in a hook, for example) leaves no trace. Turn it on in the Caddyfile, or ship aphp.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-Lengthon 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 aHEADrequest, on an answer that has aContent-LengthorTransfer-Encodingalready, or after other output went out first. - A plain
mcp/sdkscript sets it itself, before it echoes the answer:header('Content-Length: ' . strlen($body));(see the HTTP server of themcp/sdkpage). - 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 anechoin 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 ofphp -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 nexttools/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/sdkpage explains the handlers, inherited ignores anddisableSignalHandlers. The official PHP Docker images have no ext-pcntl: addRUN docker-php-ext-install pcntlto 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(orinit: truein Docker Compose), as production images should, so that an init process is PID 1. - Thread-safe (ZTS) builds, such as the official
php:*-ztsimages: 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:serverends 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’skernel.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 indisable_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()andmicrotime(). 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.