Skip to main content
This page describes the 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 tests mcp/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 and build(). 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 own tools/call handler answers give no event. A server with setReferenceHandler() stays untracked. Tracking the Builder before build() avoids both.
  • Your own tools/call and tools/list handlers 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 as nyholm/psr7, and a session store that all requests share, such as FileSessionStore:
AgentCat reads the request of a plain 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 every laravel/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/mcp 1.x has no MCP sessions. Over HTTP, the client name comes only from the _meta of each call.
  • A handler in the $methods property of your server keeps its events. A handler that you set in boot() replaces AgentCat’s handler, so that method gives no event.

Symfony

The bundle tracks every server of mcp.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.
We recommend %env(default::NAME)% for every AgentCat value from the environment. A bare %env(NAME)% with an unset variable makes the whole agentcat configuration unreadable, and AgentCat tracks no server.
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_time below 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 from identify, or anonymous.
  • Shutting down with <n> events still in queue: the 5-second budget ran out.

Testing

AgentCat does nothing under PHPUnit or Pest, or when APP_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:
The 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 null project ID reads AGENTCAT_PROJECT_ID. In TypeScript, it means no project.
  • Every page of tools/list is read under PHP-FPM, so your own tools/list handler runs once for each page.
  • A context that 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 to https://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-bundle 0.15 is not in the test matrix yet, and a new install picks it. You can stay on the tested range with composer require symfony/mcp-bundle:^0.14.
The PHP SDK README has the full runtime notes, including hardened hosts, signal handling, and the FrankenPHP worker loop.