agentcat/sdk Composer package, version 1.x. For the first setup, see PHP setup.
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.
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.
Framework and transport details
The SDK needs ext-curl, ext-json, and ext-mbstring, and we recommend ext-pcntl for stdio servers. CI testsmcp/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 andbuild(). 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 owntools/callhandler answers give no event. A server withsetReferenceHandler()stays untracked. Tracking the Builder beforebuild()avoids both. - Your own
tools/callandtools/listhandlers 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 asnyholm/psr7, and a session store that all requests share, such as FileSessionStore:
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 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 everylaravel/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 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/mcp1.x has no MCP sessions. Over HTTP, the client name comes only from the_metaof each call.- A handler in the
$methodsproperty of your server keeps its events. A handler that you set inboot()replaces AgentCat’s handler, so that method gives no event.
Symfony
The bundle tracks every server ofmcp.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.
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.- 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_timebelow 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 fromidentify, oranonymous.Shutting down with <n> events still in queue: the 5-second budget ran out.
Testing
AgentCat does nothing under PHPUnit or Pest, or whenAPP_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:
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
nullproject ID readsAGENTCAT_PROJECT_ID. In TypeScript, it means no project. - Every page of
tools/listis read under PHP-FPM, so your owntools/listhandler runs once for each page. - A
contextthat 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 tohttps://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-bundle0.15 is not in the test matrix yet, and a new install picks it. You can stay on the tested range withcomposer require symfony/mcp-bundle:^0.14.