Skip to main content
The Laravel bridge tracks every laravel/mcp server of your application with no code: you install the package and set your project ID.

Requirements

  • PHP 8.3, 8.4 or 8.5.
  • laravel/mcp 1.x, with Laravel 12.41.1 or later, or Laravel 13. On every pull request and every night, CI tests AgentCat with the newest patch of each laravel/mcp minor release from 1.0 up.
  • ext-curl, to send the events.

Install

Laravel discovers AgentCat’s service provider. agentcat:install asks for your project ID, or takes it as an argument (php artisan agentcat:install proj_…):
  • It checks the ID: proj_ followed by 27 letters or digits.
  • It writes AGENTCAT_PROJECT_ID into .env. When the application has no .env file, it creates one with that key only, and says so.
  • It adds an empty AGENTCAT_PROJECT_ID= to .env.example, when the application has one.
  • With --config, it also publishes config/agentcat.php.
  • It sends nothing over the network.
Without the command, set AGENTCAT_PROJECT_ID in .env yourself. Without a project ID, no event goes to AgentCat; your exporters still get them.

What AgentCat tracks

  • Every laravel/mcp server that the container resolves, also when laravel/mcp builds the server for each HTTP request. The server of Laravel Boost is excluded by default.
  • Every tool call. AgentCat adds a session_id and a context parameter to the input schema of each tool (and agent_id with enable_agent_tracking), and removes them from the arguments before your tool runs. It adds the get_more_tools tool.
  • After the response. AgentCat runs your hooks and sends the events in Laravel’s terminating() callbacks, after the client has its answer.
AgentCat reads the headers, method and URL of a call from the request of the server’s own HTTP transport, not from $_SERVER, so Octane workers record them too.

Configuration reference

To change the defaults, publish the configuration file:
Every key of config/agentcat.php: The file is safe with php artisan config:cache: the keys that hold environment variables read them through env(), and the service provider hands the configured values to AgentCat when the application boots. A value of the wrong type, or a word that is not in the list, logs one warning line and AgentCat uses the default. When enabled holds a word that is not in the list, the AGENTCAT_ENABLED variable of the process decides, and without one, the default. An unknown key logs WARN: Unknown AgentCat option "agentcat.<key>" ignored.

exclude and projects

exclude lists the server classes that AgentCat never tracks. A class also excludes its subclasses. An excluded server touches neither the AgentCat log nor diagnostics.
  • null, or no exclude key, is the default list: Laravel Boost’s server.
  • [] excludes nothing, Laravel Boost’s server included.
  • A blank string names no server: AgentCat logs one warning and uses the default list.
projects sends the events of some servers to another project. The first class that the server is an instance of wins. A null project (env() of an unset variable) means project_id.

The log file

AgentCat writes its own lines to storage/logs/agentcat.log, through a Laravel single channel that it builds. They stay out of your application’s log and its error trackers: laravel/mcp builds a server for each HTTP request, so the setup lines repeat on every request. To route them into one of your channels, set log_channel, for example to stack.

Hooks are invokable classes

A hook in config/agentcat.php is the class name of an invokable class. A closure cannot go in this file, because php artisan config:cache cannot store it. AgentCat resolves the class from the container when the hook runs (under Octane, from the container of the request), and calls it with the same argument as the PHP hook: an AgentCat\ToolCall for identify, resolve_session_id, event_tags and event_properties, a string for redact_sensitive_information, and an AgentCat\Event for redact_event.
A hook class that cannot be resolved, or that throws, logs one line when the hook runs. The tool call is never affected.

Exporters

In config/agentcat.php, an exporter is an array with a type and the fields of its integration page, with the TypeScript field names:
An entry with an unknown type, or a missing field, logs one line and is skipped; the other entries still work.

Octane and other long-running workers

Under Laravel Octane (Swoole, RoadRunner or FrankenPHP), a worker serves many requests. After each request, AgentCat waits at most 1 second in terminating() for its delivery, so the worker is soon free for the next request.
  • The wait shows in request durations. The worker’s time for a request that sends events can be up to 1 second longer, in Octane’s numbers and in your monitoring. The client has its answer before the wait.
  • An idle worker holds its last request’s events. Events that are still waiting after the 1 second go out at the next request of the worker, or when the worker stops.
  • When a worker stops, AgentCat delivers what is left for at most 5 seconds, at Octane’s WorkerStopping event. Measured on Swoole 6.2.3: when the whole server stops, Swoole did not cut that delivery. A worker that Swoole restarts (for example after Octane’s --max-requests) is stopped by force about twice its max_wait_time server setting after it is told to stop: about 6 seconds with the default of 3. So a max_wait_time below 3 seconds can cut the delivery of a restarting worker short, and with the default, the other WorkerStopping listeners leave AgentCat its 5 seconds only when they take less than about 1 second.
Under PHP-FPM, FrankenPHP’s classic mode and Apache, AgentCat does not wait in terminating(): it only starts the delivery there, and finishes it at the end of the script, after the response. See PHP runtimes. AgentCat binds one method object per tracked server in the container, under a key with the server’s object ID. In a worker, the binding of a server that is gone stays until PHP reuses its object ID or the container ends.

Your own method handlers

A tools/list, tools/call or initialize class that your server declares in its $methods property is wrapped: AgentCat delegates to it and records one event per call. For your own tools/call class, the event of a failing tool carries the error of the answer, not the original exception. A method handler that you set in the server’s boot() method replaces AgentCat’s for that server: laravel/mcp calls boot() after AgentCat has tracked the server. AgentCat then records no event for that method, and logs nothing:
Declare such a handler in the $methods property instead.

ToolSearch catalogs

A server that exposes its tools through a laravel/mcp ToolSearch catalog has the two tools search_tools and execute_tools. AgentCat injects its parameters into them like into any other tool. One execute_tools call is one event: AgentCat does not record the tool calls that run inside it one by one.

Streamed answers

A tool whose handle() is a generator answers with a stream. Its notifications pass unchanged, and AgentCat makes the event from the final result, so the event’s duration includes the whole stream. A stream that the client abandons publishes no event.

Client name and version

laravel/mcp 1.x has no MCP sessions. AgentCat takes the client’s name and version from the _meta of the call, else from the initialize request of an older client to the same server instance. A stdio server keeps that for the whole process. Over HTTP it is lost, because each request builds a new server. The events have no transport session ID and no auth object: the Authorization header is in the request headers.

Responses on Apache mod_php, php-cgi and php -S

laravel/mcp sends its JSON answers without a Content-Length header. Where PHP sends a response only when the script ends, the bridge gives the plain JSON answers of a tracked server their Content-Length, so the client has the whole answer before AgentCat delivers. PHP runtimes has the rules and the runtimes where it does that.

Testing

AgentCat is off while your tests run (PHPUnit, Pest, or APP_ENV=testing): your servers answer as without AgentCat, and laravel/mcp’s test helpers see their own answers. To assert on the events of a test, call AgentCat::fake() before the test builds a server, and restore the real client in tearDown():
The mcp/sdk page says what each assertion of the fake counts. Three things to know in a Laravel suite:
  • Two settings outlive the test that made them. The fake stays the shared client of the PHP process until AgentCat::setClient(null). The agentcat.enabled value that the service provider reads when the application boots stays in effect until the next application boots.
  • With the fake, AgentCat changes the answers as in production. A call without a session_id gets the issued session ID as the first text block of content. A tool with an output schema gets an mcp_session entry in its structuredContent. An assertion on the exact structuredContent (for example assertStructuredContent()) then sees that entry.
  • A bad value in config/agentcat.php logs one line per test application, also without the fake, because each test boots a new application. The lines go to storage/logs/agentcat.log.

Laravel Boost guidelines

The package ships guidelines for AI coding agents in resources/boost/guidelines/core.blade.php. When you run php artisan boost:install, Laravel Boost adds them to your agent’s guidelines, so your coding agent knows how AgentCat is set up: the install command, the configuration keys, the hooks as invokable classes, and AgentCat::fake() in tests.