> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcat.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PHP: Symfony

> Track every symfony/mcp-bundle server of a Symfony application: setup, the configuration reference, hooks as services and the logs.

The Symfony bundle tracks every [`symfony/mcp-bundle`](https://github.com/symfony/mcp-bundle) server of your application with no code: you register the bundle and set your project ID.

## Requirements

* PHP 8.3, 8.4 or 8.5.
* `symfony/mcp-bundle` 0.13 or 0.14, with Symfony 7.3 or 8.x. On every pull request and every night, CI tests AgentCat with the newest patch of each `symfony/mcp-bundle` minor release from 0.13 up.
* ext-curl, to send the events.

`symfony/mcp-bundle` 0.14 requires `mcp/sdk` 0.8, so a Symfony application stays on `mcp/sdk` 0.8 until the bundle releases a version for `mcp/sdk` 0.9.

## Setup

Install the package:

```bash theme={null}
composer require agentcat/sdk
```

Register the bundle in `config/bundles.php`:

```php theme={null}
return [
    // ...
    AgentCat\Symfony\AgentCatBundle::class => ['all' => true],
];
```

Then set your project ID in `.env.local`, or in the environment of your server:

```dotenv theme={null}
AGENTCAT_PROJECT_ID=proj_YOUR_PROJECT_ID
```

That is all: every server of the `mcp.servers` configuration is tracked. AgentCat adds a `session_id` and a `context` parameter to each tool, removes them from the arguments before your tool runs, and adds the `get_more_tools` tool. It runs your hooks and sends the events on `kernel.terminate`, after the response.

A `config/packages/agentcat.yaml` file is optional. It holds the settings below, under the `agentcat` key.

## Configuration reference

`php bin/console config:dump-reference agentcat` prints every key with its default:

```yaml theme={null}
# Default configuration for extension with alias: "agentcat"
agentcat:

    # On or off for every MCP server and every AgentCat call: true or false. Unset: on, except while your tests run.
    enabled:              '%env(default::AGENTCAT_ENABLED)%'

    # Your AgentCat project ID (proj_…). Unset: no events go to AgentCat; exporters still run.
    project_id:           '%env(default::AGENTCAT_PROJECT_ID)%'

    # Servers AgentCat never tracks: names of mcp.servers entries; a string is one name. Names written here are read when the container compiles, names from environment variables when each server is built.
    exclude_servers:      []

    # Another project for some servers: mcp.servers name => project ID. A null project is project_id.
    projects:

        # Prototype
        server:               ~

    # The service ID of a PSR-3 logger for AgentCat's own lines, for example monolog.logger.mcp, written here, not in an environment variable. Unset: their own file, agentcat.log in the log directory.
    logger:               null

    # The AgentCat API. Unset: AgentCat's. MCPCAT_API_URL, the TypeScript SDK's name, is read too.
    api_url:              '%env(default::AGENTCAT_API_URL)%'

    # Adds the get_more_tools tool, so agents can report tools they miss. Default: true. Takes true, false, 1, 0, yes, no, on, off or an %env()% value.
    enable_report_missing: true

    # Publishes one event for every tool call. Default: true. Takes true, false, 1, 0, yes, no, on, off or an %env()% value.
    enable_tracing:       true

    # Adds a context parameter to your tools, so agents explain each call. Default: true. Takes true, false, 1, 0, yes, no, on, off or an %env()% value.
    enable_tool_call_context: true

    # Replaces the default description of the context parameter. Unset: the default description.
    custom_context_description: null

    # Adds an agent_id parameter to your tools, to tell agents apart. Default: false. Takes true, false, 1, 0, yes, no, on, off or an %env()% value.
    enable_agent_tracking: false

    # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs.
    identify:             null

    # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs.
    resolve_session_id:   null

    # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs.
    event_tags:           null

    # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs.
    event_properties:     null

    # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs.
    redact_sensitive_information: null

    # The service ID of an invokable service, written here, not in an environment variable. The service is resolved when the hook runs.
    redact_event:         null

    # Your own telemetry tools: name => {type: otlp|datadog|sentry|posthog, …}. A ~ entry is skipped.
    exporters:            null

    # Diagnostics: AgentCat's own log lines, sent to AgentCat so that it can find broken installs. true turns them off; in a list, one true value is enough. DISABLE_DIAGNOSTICS, the TypeScript SDK's name, is read too.
    disable_diagnostics:

        # Defaults:
        - %env(default::AGENTCAT_DISABLE_DIAGNOSTICS)%
        - %env(default::DISABLE_DIAGNOSTICS)%

    # Where diagnostics go: an OpenTelemetry collector (OTLP/HTTP logs). Unset: AgentCat's.
    diagnostics_endpoint: '%env(default::AGENTCAT_DIAGNOSTICS_ENDPOINT)%'

    # The bearer token of diagnostics_endpoint. Unset: AgentCat's.
    diagnostics_token:    '%env(default::AGENTCAT_DIAGNOSTICS_TOKEN)%'

    # Keeps AgentCat from installing its SIGTERM and SIGINT handlers in CLI servers. Default: false. Takes true, false, 1, 0, yes, no, on, off or an %env()% value.
    disable_signal_handlers: false
```

A value of the wrong type logs one warning line, at the first server that AgentCat tracks, and AgentCat uses the default.

### Write `%env(default::…)%` for every AgentCat value

Take each AgentCat value from the environment with the `default::` processor, as the defaults above do:

```yaml theme={null}
agentcat:
    projects:
        billing: '%env(default::AGENTCAT_BILLING_PROJECT_ID)%'
```

> [!WARNING]
>
> A bare `%env(NAME)%` whose variable is not set makes the whole `agentcat` configuration unreadable. AgentCat then tracks no server at all: every server answers without AgentCat, and AgentCat logs one line for each server. With `default::`, an unset variable is `null`, which means the default.

### `exclude_servers` and `projects`

Both use the names of the `mcp.servers` entries of your `symfony/mcp-bundle` configuration:

```yaml theme={null}
agentcat:
    exclude_servers: [internal]
    projects:
        billing: '%env(default::AGENTCAT_BILLING_PROJECT_ID)%'
```

A server that `exclude_servers` names in the configuration itself is left out when the container compiles: AgentCat never touches it. A name that comes from an environment variable is read when the server is built. A `null` project is `project_id`.

### Hooks are invokable services

A hook is the service ID of an invokable service, written in the configuration (not in an environment variable). AgentCat gets the service when the hook runs, and calls it with the same argument as the [PHP hook](/sdk/identifying-users.md): 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`.

```php theme={null}
namespace App\Mcp;

use AgentCat\Identity;
use AgentCat\ToolCall;

final class IdentifyActor
{
    public function __invoke(ToolCall $call): ?Identity
    {
        $token = $call->headers['authorization'] ?? null;

        return $token === null ? null : new Identity(userId: hash('sha256', $token));
    }
}
```

```yaml theme={null}
agentcat:
    identify: App\Mcp\IdentifyActor
```

With autowiring, the class name is the service ID. A missing service never breaks the container: AgentCat logs one line when the hook runs, and the tool call is not affected.

**Read the request from the `ToolCall`, not from the `RequestStack`.** AgentCat runs the hooks after the response, on `kernel.terminate` or in its shutdown function, where the `RequestStack` is empty. `$call->headers` holds the headers of the call, with lower-case names.

## The log file

AgentCat writes its own lines to `%kernel.logs_dir%/agentcat.log` (for example `var/log/agentcat.log`), not to the `mcp` logger of the bundle. Under PHP-FPM, each request builds the container and tracks the servers again, so the setup lines repeat on every request; in their own file, they stay out of your application's log and its error trackers.

* **To route them elsewhere**, set `logger` to the service ID of a PSR-3 logger, for example `monolog.logger.mcp`. An ID that is not a PSR-3 logger logs `WARN: AgentCat option "agentcat.logger" must be the ID of a PSR-3 logger service, got "<id>"; the default is used` to the default file.
* **To give the `mcp` channel a file of its own**, with `symfony/mcp-bundle`'s lines (and AgentCat's, with `logger: monolog.logger.mcp`), add a Monolog handler for that channel:

```yaml theme={null}
monolog:
    handlers:
        mcp:
            type: stream
            path: '%kernel.logs_dir%/mcp.log'
            channels: ['mcp']
```

* **Lines written before any server is built** go to `~/agentcat.log`, for example the line of an `AgentCat::publishCustomEvent()` call in a request that serves no MCP call.

## When AgentCat tracks a server

AgentCat tracks a server when the container builds its Builder: at an MCP request, in `bin/console mcp:server`, or in the debug tools. It never tracks at boot, so a console command or a worker that builds no MCP server gets no AgentCat setup, no diagnostics and no signal handlers. `bin/console list` builds no server.

In debug mode, the profiler builds every server's Builder at the end of each request, and `bin/console debug:mcp` builds them all too. AgentCat then tracks them: you see the setup lines, and with diagnostics on, one diagnostics request, also for dev pages that serve no MCP call.

## The order of request handlers

`mcp/sdk` answers each request with the first handler that supports it. AgentCat's compiler pass runs after the bundle's own passes (at priority -100, before optimization), and it adds AgentCat's handlers before the request handlers that you tag `mcp.request_handler`. So a `tools/call` or `tools/list` handler of yours with that tag comes after AgentCat's: AgentCat delegates to it, your handler answers, and AgentCat records one event per call.

A handler that your own compiler pass adds with an `addRequestHandler()` method call on a builder stays ahead of AgentCat's handlers. For `tools/call` or `tools/list`, it answers first, and AgentCat records nothing for those requests. Tag the handler `mcp.request_handler` instead.

## After the response

AgentCat delivers on `kernel.terminate`, in a listener that runs last (priority -2048):

* **In Symfony Runtime workers** (RoadRunner, Swoole, FrankenPHP worker mode), it waits at most 1 second after each request, so the worker is soon free. Events still waiting go out at the next request of the worker, or when the worker stops.
* **Under PHP-FPM, FrankenPHP's classic mode and the other per-request runtimes**, it only starts the delivery there. Its shutdown function finishes it after the response, within 5 seconds.

AgentCat reads the headers of each call from the main request of the `RequestStack`, so a worker records the request it serves. `bin/console mcp:server` (stdio) has no request and records none.

### Responses on Apache mod\_php, php-cgi and `php -S`

There, PHP sends a response only when the script ends, and `kernel.terminate` runs before the client has it. The bundle gives the plain JSON answers of a tracked server their `Content-Length`, so the client has the whole answer before `kernel.terminate` and AgentCat's delivery. [PHP runtimes](/sdk/php/runtimes.md) has the rules.

## Testing

AgentCat is off while your tests run (PHPUnit, Pest, or `APP_ENV=test`). To assert on the events of a functional test, call `AgentCat::fake()` before the kernel builds a server, and restore the real client with `AgentCat::setClient(null)` in `tearDown()`. The [`mcp/sdk` page](/sdk/php/mcp-sdk#testing) says what each assertion of the fake counts.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.