In March 2025 I built mcp-profiler-bundle, a Symfony bundle that let AI coding agents read the Symfony Profiler: the requests your app just served, the queries they ran, the exceptions they threw. The bet was simple. An agent that can see what the app actually did should debug it better than one that can only read the source.
This week I deprecated it. Symfony now ships the official version of that idea, and rereading my code for the deprecation notice left me with an uncomfortable verdict: I was right about the problem, and half-wrong about the solution.
MCP, the Model Context Protocol, is a JSON-RPC protocol that lets a client like Claude Desktop, Cursor or Cline call tools on a local server. My bundle added a bin/console mcp:server:run command that spoke it over stdio and exposed four tools: profiler:list, profiler:get_by_token, profiler:get_collectors and profiler:get_collector.
There was no official PHP SDK back then. My first commit leaned on a community library, james2037/mcp-php-server, which only existed as dev-master. By the 0.1.0 tag on 31 March 2025 it was gone, and the whole protocol fit in one match:
// Command/RunMCPServerCommand.php
$payload = json_decode($line, true, JSON_THROW_ON_ERROR);
$method = $payload['method'] ?? null;
$response = match ($method) {
'initialize' => $this->sendInitialize(),
'tools/list' => $this->sendToolsList(),
'tools/call' => $this->callTool($payload['params'] ?? []),
'notifications/initialized' => null,
default => $this->sendProtocolError(\sprintf('Method "%s" not found', $method)),
};
Hold on to that first line. It comes back later.
It found some users: 11 stars, 2 forks and roughly 5,900 Packagist installs. Version 0.2.0 added multi-kernel (APP_ID) support a week after launch, and in February 2026 an outside contributor, Rhodri Pugh, sent a PHP 8.4 fix.
The idea is official now. In September 2025, Symfony announced the official PHP MCP SDK, built with The PHP Foundation and Anthropic's MCP team. On 23 December 2025, Symfony AI Mate shipped its first release, about nine months after my 0.1.0: an MCP dev server giving coding agents access to a Symfony app's internals. The container came first, profiler access landed in 0.3 in January 2026, and it handles multi-kernel apps too.
Symfony's spotlight on Mate opens on the same premise: an agent can read all your code and still not know what happened on the last request. Mate is past 100,000 installs a month.
In August 2026, Mate 0.13 deleted its MCP server (PR #2380). It no longer depends on mcp/sdk, and the agent runs plain commands instead, like these from Mate's docs and its profiler skill:
vendor/bin/mate tools:list
vendor/bin/mate tools:call symfony-profiler-list --statusCode=500 --limit=5
vendor/bin/mate resources:read symfony-profiler://profile/<token>
A bare CLI has a problem an MCP server doesn't: nothing tells the agent it exists. Mate's answer is Agent Skills — SKILL.md files installed into .agents/skills/ (mirrored into .claude/skills/) that say when to reach for which tool. The profiler skill reads like a triage procedure: filter for the failing request instead of scrolling, check which collectors it has, then read them in symptom order. exception first for a 5xx; time, db, then memory for a slow request.
Johannes Wachter, who made that change, then measured it: Claude Haiku, two tasks with injected performance bugs, ten runs per setup, counting whether the agent touched Mate at all. The bare CLI: 0 runs out of 20. The MCP server: 5 out of 20. The CLI with Skills and project instructions: 10 out of 10 on one task, 5 out of 10 on the harder one. He calls it a pattern from a small sample, not a proven effect, and concludes that "transport alone wasn't the deciding variable. Visibility was."
That's the half I got wrong. I treated exposing the data as the product. Tools give the agent eyes; skills tell it where to look. My bundle handed over four tools with one-line descriptions ("Lists available profiler tokens.") and left the agent to work out the rest.
mcp-profiler-bundle (2025) |
Symfony AI Mate 0.13 (2026) | |
|---|---|---|
| How the agent calls it | MCP over stdio, hand-written JSON-RPC | plain CLI, no server process |
| Needs the app to boot | yes, it's a bin/console command |
no, reads the profiler and the dumped container from disk |
| Finding a profile | the last N ( limit ) |
method, URL, IP, status, date range, kernel context |
| Collector output | raw collector data or VarDumper dumps | formatters for Doctrine, exceptions, logs, mail, memory, requests, timing and translations |
| Secrets | no redaction | cookies, session data, auth headers and secret env vars redacted |
| Prompt injection | nothing | output wrapped and marked as untrusted data |
| Telling the agent what to do | four one-line tool descriptions | Agent Skills plus AGENTS.md /CLAUDE.md |
| Beyond the profiler | nothing | service container, Monolog logs |
Rereading the code was humbling. PHPStan flags the first two of these problems in CI, on a step I'd marked continue-on-error: true the day I set up CI:
------ -----------------------------------------------------------------------
Line Command/RunMCPServerCommand.php
------ -----------------------------------------------------------------------
40 While loop condition is always true.
🪪 while.alwaysTrue
61 Unreachable statement - code above always terminates.
🪪 deadCode.unreachable
68 Constant JSON_THROW_ON_ERROR is not allowed for parameter #3 $depth
of function json_decode.
🪪 argument.invalidConstant
fgets() returns false, and the loop sleeps a millisecond and tries again, forever.JSON_THROW_ON_ERROR sits in json_decode()'s $depth parameter, not $flags, so bad input decodes to null. Instead of a -32700 parse error with a -32601 Method "" not found with id 0. And any other notification, like notifications/cancelled, gets an error reply, which JSON-RPC 2.0 forbids. profilerlist and profilerget_by_token, and the README documented different names altogether. ip, url, method and status_code. The rewrite kept them in ProfilerList::execute() but put only limit in the tool schema..env values into $_SERVER, that includes APP_SECRET and DATABASE_URL. profiler:get_collector dumped whatever the agent asked for, unfiltered, and the optional bin/run-mcp.sh wrapper (adapted from var/mcp/stdout.log. bin/console didn't start, and neither did the server. Mate reads everything from disk, so it still answers when the app won't boot.
The protocol has moved on, too. MCP's 2026-07-28 revision removed the initialize handshake my match is built around and made a new server/discover method mandatory.
None of this is a verdict against MCP, by the way. symfony/mcp-bundle, for building your own MCP servers, does over 600,000 installs a month. The shift is narrower: for a dev tool the agent runs on your machine, a CLI plus skills got picked up at least as reliably, without a server process to babysit.
So I retired it properly. PR #14 puts a deprecation notice at the top of the README, with the migration:
composer require --dev symfony/ai-mate
vendor/bin/mate init
composer require --dev symfony/ai-symfony-mate-extension symfony/ai-monolog-mate-extension
vendor/bin/mate discover
mate discover also installs the skills. For frontend work, Simon André's Symfony UX skills cover Stimulus, Turbo, Twig Components and Live Components.
Even deprecating it took a fix. CI had been red since June, failing on the PHP CS Fixer step, and the log on the deprecation PR showed the culprit: with no config file, PHP CS Fixer 3.95 opens an interactive init wizard, which crashes in CI with Failed to read template file. A .php-cs-fixer.dist.php turned it green.
0.2.1 is the final release, Rhodri's fix included, and the repository stays public. The lesson I'm keeping: access was the easy half. The half that makes it useful is deciding what the agent should look at, in what order, and what it must never see.