Skip to content

fix(mcp): make tools/list resilient to an empty registry#8371

Open
soyuka wants to merge 2 commits into
api-platform:4.3from
soyuka:fix-mcp-tools-list-8370
Open

fix(mcp): make tools/list resilient to an empty registry#8371
soyuka wants to merge 2 commits into
api-platform:4.3from
soyuka:fix-mcp-tools-list-8370

Conversation

@soyuka

@soyuka soyuka commented Jun 30, 2026

Copy link
Copy Markdown
Member
Q A
Branch? 4.3
Bug fix? yes
New feature? no
Deprecations? no
Issues Refs #8370
License MIT

Problem

Reported in #8370: under FrankenPHP worker mode, MCP tools/list returns an empty array while tools/call keeps working.

tools/list reads from the SDK registry, which is populated once, when the shared mcp.server service is built (Builder::build() runs all loaders into the shared mcp.registry). Under a persistent runtime the kernel boots once, so that build happens once — and if it runs while the API Platform metadata is not yet available (e.g. a cold cache pool during worker warmup), the registry captures an empty state and stays empty for the whole process.

tools/call is unaffected because the request-time Handler (tagged mcp.request_handler) resolves operations from resource metadata directly, independent of the registry. tools/list had no equivalent request-time path — hence the asymmetry.

Note on the trigger. I could not reproduce an empty registry from the 4.3.14 diff alone: the SDK build() lifecycle is structurally unchanged across mcp/sdk 0.4→0.6, and a worker simulation (boot once + services_resetter + prod) returns the full tool set through the SDK's own handler. The empty state appears to be triggered by build-time/warmup ordering specific to a real worker deployment rather than by a code regression. This PR therefore makes tools/list resilient regardless of the trigger, mirroring how tools/call already works. A minimal FrankenPHP reproducer from the reporter would help confirm the exact ordering.

Fix

Add ApiPlatform\Mcp\Server\ListHandler, tagged mcp.request_handler so it precedes the SDK's registry-backed list handlers. On first use it loads the API Platform elements into the registry (idempotent, once per process), then reads back through the shared mcp.registry — so:

  • an empty/cold registry is healed at request time;
  • tools registered at runtime and registry decorators (e.g. dynamically discovered tools) remain visible;
  • the loader runs once per process, not on every request.

Covered by unit tests, including a case asserting runtime-registered tools stay visible and a case asserting the loader runs once.

tools/list reads from the SDK registry, which is populated once, when
mcp.server is built. Under a persistent runtime (e.g. FrankenPHP worker
mode) that single build can capture an empty registry and stay empty for
the whole process, so tools/list returns [] while tools/call keeps
working through the request-time Handler.

Add a ListHandler (tagged mcp.request_handler, so it precedes the SDK's
registry-backed list handlers) that loads API Platform elements into the
registry on first use and reads back through the shared registry, so
runtime registrations and registry decorators are preserved.

Refs api-platform#8370
@soyuka soyuka force-pushed the fix-mcp-tools-list-8370 branch from ffedb71 to 5544a15 Compare July 1, 2026 05:44
@soyuka soyuka changed the title fix(mcp): list tools/resources at request time Jul 1, 2026
Comment thread src/Mcp/Server/ListHandler.php
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant