> ## 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

> Set up AgentCat in a PHP MCP server built with mcp/sdk, Laravel, or Symfony, with identity and every feature on.

> [!TIP]
>
> Your coding agent can do the setup for you with this prompt:
>
> ```text theme={null}
> Read and follow https://docs.agentcat.com/languages/php.md to set up AgentCat in this MCP server. Follow its Guidance for AI agents.
> ```

## Supported MCP SDKs

| MCP library | Versions | Transports | Runtime |
| - | - | - | - |
| `mcp/sdk` (official) | 0.8.1 and later | stdio, streamable HTTP, stateless HTTP | PHP 8.3 to 8.5 |
| `laravel/mcp` | 1.x, on Laravel 12 (12.41.1 and later) or 13 | HTTP (`Mcp::web`), stdio (`Mcp::local`) | PHP 8.3 to 8.5 |
| `symfony/mcp-bundle` | 0.13 and 0.14, on Symfony 7.3 or 8.x | HTTP, stdio (`bin/console mcp:server`) | PHP 8.3 to 8.5 |

One `agentcat/sdk` package supports all three. AgentCat does not test `symfony/mcp-bundle` 0.15 yet, and the [known limits](https://docs.agentcat.com/languages/php/reference.md#known-limits) show how to stay on 0.14.

## Installation

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

The SDK needs your project ID, which you can find in the [AgentCat dashboard](https://agentcat.com/app/settings/projects/). We recommend the `AGENTCAT_PROJECT_ID` environment variable, which keeps the ID out of your code. The SDK reads it by itself.

## Adding AgentCat to your server

With `mcp/sdk`, `AgentCat::track()` adds AgentCat to the `Mcp\Server\Builder` where your code creates it. In Laravel and Symfony, the bridge tracks every server, also one built per request, so you add no `track()` call:

```php mcp/sdk theme={null}
use AgentCat\AgentCat;
use Mcp\Server;

$builder = Server::builder()->setServerInfo('my-server', '1.0.0');
// $builder->addTool(...) and your other capabilities

AgentCat::track($builder); // reads AGENTCAT_PROJECT_ID

$builder->build()->run($transport);
```

```bash Laravel theme={null}
composer require agentcat/sdk
php artisan agentcat:install # writes your project ID to .env
```

```php Symfony: config/bundles.php theme={null}
// Symfony Flex contrib recipes add this line for you
return [
    // ...
    AgentCat\Symfony\AgentCatBundle::class => ['all' => true],
];
```

```dotenv Symfony: .env.local theme={null}
AGENTCAT_PROJECT_ID=proj_YOUR_PROJECT_ID
```

We recommend that you track the Builder before `build()`, because after `build()` [a few calls give no event](https://docs.agentcat.com/languages/php/reference.md#official-php-mcp-sdk). Under PHP-FPM, each request tracks a new server, as a stateless server needs. The reference page has a [complete HTTP script](https://docs.agentcat.com/languages/php/reference.md#http-servers).

## Identifying users

> [!NOTE]
>
> We strongly recommend that you identify your users. Without identity, the dashboard cannot show who uses your server, and each session stays anonymous.

The `identify` hook tells AgentCat which user made each tool call. We recommend the first of these two levels that your server can give.

### Full identity (recommended)

Full identity gives each session a real user ID and name, read from the request. In Laravel and Symfony, the hook is an invokable class that the container builds:

```php mcp/sdk theme={null}
use AgentCat\AgentCat;
use AgentCat\Identity;
use AgentCat\Options;
use AgentCat\ToolCall;

AgentCat::track($builder, options: new Options(
    identify: static function (ToolCall $call): ?Identity {
        $token = str_replace('Bearer ', '', $call->headers['authorization'] ?? '');
        if ($token === '') {
            return null;
        }
        $user = findUserByToken($token); // your own lookup
        if ($user === null) {
            return null;
        }

        return new Identity(userId: (string) $user->id, userName: $user->name);
    },
));
```

```php Laravel theme={null}
namespace App\AgentCat;

use AgentCat\Identity;
use AgentCat\ToolCall;
use App\Repositories\UserRepository;

final class IdentifyUser
{
    public function __construct(private UserRepository $users) {}

    public function __invoke(ToolCall $call): ?Identity
    {
        $token = str_replace('Bearer ', '', $call->headers['authorization'] ?? '');
        if ($token === '') {
            return null;
        }
        $user = $this->users->findByToken($token); // your own lookup
        if ($user === null) {
            return null;
        }

        return new Identity(userId: (string) $user->id, userName: $user->name);
    }
}
```

```php Symfony theme={null}
namespace App\AgentCat;

use AgentCat\Identity;
use AgentCat\ToolCall;
use App\Repository\UserRepository;

final class IdentifyUser
{
    public function __construct(private UserRepository $users) {}

    public function __invoke(ToolCall $call): ?Identity
    {
        $token = str_replace('Bearer ', '', $call->headers['authorization'] ?? '');
        if ($token === '') {
            return null;
        }
        $user = $this->users->findOneBy(['apiToken' => $token]); // your own lookup
        if ($user === null) {
            return null;
        }

        return new Identity(userId: (string) $user->getId(), userName: $user->getUserIdentifier());
    }
}
```

In Laravel and Symfony, the class runs only when your configuration names it:

```bash Laravel: publish the configuration theme={null}
php artisan vendor:publish --tag=agentcat-config
```

```php Laravel: config/agentcat.php theme={null}
'identify' => App\AgentCat\IdentifyUser::class,
```

```yaml Symfony: config/packages/agentcat.yaml theme={null}
agentcat:
    identify: App\AgentCat\IdentifyUser
```

### Anonymized identity (the minimum)

If you prefer not to send user details, a stable ID works too, for example a hash of the token or API key. The same user always gets the same ID:

```php mcp/sdk theme={null}
use AgentCat\AgentCat;
use AgentCat\Identity;
use AgentCat\Options;
use AgentCat\ToolCall;

AgentCat::track($builder, options: new Options(
    identify: static function (ToolCall $call): ?Identity {
        $token = str_replace('Bearer ', '', $call->headers['authorization'] ?? '');
        if ($token === '') {
            return null;
        }

        return new Identity(userId: 'anon-' . substr(hash('sha256', $token), 0, 16));
    },
));
```

```php Laravel and Symfony theme={null}
namespace App\AgentCat;

use AgentCat\Identity;
use AgentCat\ToolCall;

final class IdentifyUser
{
    public function __invoke(ToolCall $call): ?Identity
    {
        $token = str_replace('Bearer ', '', $call->headers['authorization'] ?? '');
        if ($token === '') {
            return null;
        }

        return new Identity(userId: 'anon-' . substr(hash('sha256', $token), 0, 16));
    }
}
```

In Laravel and Symfony, your configuration names the class:

```bash Laravel: publish the configuration theme={null}
php artisan vendor:publish --tag=agentcat-config
```

```php Laravel: config/agentcat.php theme={null}
'identify' => App\AgentCat\IdentifyUser::class,
```

```yaml Symfony: config/packages/agentcat.yaml theme={null}
agentcat:
    identify: App\AgentCat\IdentifyUser
```

`$call->headers` holds the HTTP headers, with lower-case names. On stdio it is empty, so the hook can read your server's own key. If the hook throws, the event goes out without identity.

## Recommended settings

AgentCat turns on every feature by default, and we recommend that you keep them that way. Each one adds a view of how agents use your server:

* **Intent** (`enableToolCallContext`): agents tell you why they call each tool, which powers session goals.
* **Agent feedback** (`enableReportMissing`): agents can report tools that they looked for and did not find, through the `get_more_tools` tool.
* **Sessions** (`enableTracing`): AgentCat records each tool call and groups the calls of one task into one session, so you can replay it.
* **Diagnostics** (`disableDiagnostics` turns it off): the SDK's own log lines, which [can include user IDs](https://docs.agentcat.com/languages/php/reference.md#diagnostics), help us fix a broken install faster.

If you need to turn one off, the [reference page](https://docs.agentcat.com/languages/php/reference.md#options) explains each option.

## Testing your setup

The PHP SDK always logs its setup and each event that it sends, so it needs no debug option.

1. Watch the log:

```bash theme={null}
tail -f ~/agentcat.log              # mcp/sdk
tail -f storage/logs/agentcat.log   # Laravel
tail -f var/log/agentcat.log        # Symfony
```

2. Start your real server with `AGENTCAT_PROJECT_ID` set, as you start it in production.
3. Call one tool with an MCP client, for example the MCP Inspector CLI:

```bash theme={null}
npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http \
  --header "Authorization: Bearer <token>" \
  --method tools/call --tool-name <tool> --tool-arg <name>=<value> \
  --tool-arg session_id=start --tool-arg "context=Testing that AgentCat records this tool call."
```

For a stdio server, you can replace the URL and `--transport http` with the command that starts your server, and leave out `--header`.

4. Look for `Successfully sent event ... | mcp:tools/call | ...` in the log. The line ends with the user ID from `identify`, or `anonymous`. In [Sessions](https://agentcat.com/app/sessions/), the new session shows the user name.

A coding agent can confirm the event with the [AgentCat MCP server](https://docs.agentcat.com/agent/connect.md). If no event arrives, the log shows whether the setup or a send failed.

## Guidance for AI agents

1. Keep `identify` in place, because without it every user is anonymous; if the developer does not want to send user details, offer the anonymized ID.
2. Return the same ID for the same user on every call, because a random value or a timestamp splits one user into many.
3. Keep raw tokens, API keys, and passwords out of `userId`, `userName`, and `userData`, because AgentCat stores and shows them.
4. Leave `enableToolCallContext`, `enableReportMissing`, and `enableTracing` at their defaults, because each one feeds a part of the dashboard.
5. Read the project ID from `AGENTCAT_PROJECT_ID`, so that it stays out of committed code.
6. Add no `track()` call in Laravel or Symfony, because the bridge tracks every server.
7. Give Laravel and Symfony hooks as an invokable class, because the bridge cannot store a closure.
8. Install `agentcat/sdk` without a version constraint, because a version from memory can be old.
9. Test through the real server start, outside PHPUnit and Pest, because tests turn AgentCat off and a separate script can skip `track()`.

## Next steps

> [card] [PHP reference](https://docs.agentcat.com/languages/php/reference.md)
>
> Options, frameworks, runtimes, and limits

> [card] [Custom events](https://docs.agentcat.com/sdk/custom-events.md)
>
> Send your own events

> [card] [Tags and properties](https://docs.agentcat.com/sdk/event-tags-properties.md)
>
> Add data that you can filter on

> [card] [Redaction](https://docs.agentcat.com/privacy-security/client-side-redaction.md)
>
> Remove sensitive data in your server

> [card] [Exporters](https://docs.agentcat.com/integrations.md)
>
> Copies of events for OpenTelemetry, Datadog, Sentry, or PostHog


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