Skip to content

[MCP] tools/list returns empty list on api-platform/core 4.3.14–4.3.15 (4.3.13 OK) #8370

Description

@laryjulien

Summary

Since upgrading api-platform/core from 4.3.13 to 4.3.15, the MCP endpoint's
tools/list returns an empty tools array for every tool, so MCP clients/agents
can no longer discover any tool.

The tools are still callabletools/call works fine for every tool — which makes
the regression easy to miss: an agent that already knows a tool name can invoke it, but
nothing is discoverable. initialize even advertises the tools capability
("tools":{"listChanged":true}), yet the subsequent tools/list is empty.

Pinning api-platform/core back to 4.3.13 fully restores discovery (all tools
returned by tools/list). The regression seems introduced in 4.3.14.

Environment

api-platform/core 4.3.14 broken, 4.3.13 OK
mcp/sdk 0.5.0
symfony/mcp-bundle 0.9.0
Symfony 8.1
PHP 8.5.7 (ZTS)
Runtime FrankenPHP, worker mode (dunglas/symfony-docker)
api_platform.mcp enabled: true, format: jsonld

Tools are declared via the mcp: array on MCP-only resources, e.g.:

#[ApiResource(operations: [], mcp: [
    'list-talks' => new McpToolCollection(
        name: 'list-things',
        description: '',
        input: ListThingsInput::class,
        processor: ListThingsProcessor::class,
        structuredContent: true,
    ),
])]
final class Thing { /* … */ }

Steps to reproduce

JSON-RPC over HTTP against /mcp (FrankenPHP worker mode):

# 1. initialize → capture Mcp-Session-Id
curl -s -D - -o /dev/null -X POST http://localhost/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"cli","version":"1.0"}}}'
# response advertises: "capabilities":{... "tools":{"listChanged":true} ...}

# 2. notifications/initialized (same session)
curl -s -X POST http://localhost/mcp -H 'Mcp-Session-Id: <SID>' … \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'

# 3. tools/list (same session)
curl -s -X POST http://localhost/mcp -H 'Mcp-Session-Id: <SID>' … \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'

Expected

tools/list returns the full set of registered tools.

Actual

{"jsonrpc":"2.0","id":2,"result":{"tools":[]}}

Empty on 4.3.14 and 4.3.15. On 4.3.13 the same request returns all tools.
tools/call (e.g. {"name":"list-talks","arguments":{}}) works on both versions.

Diagnosis / observations

The SDK registry that backs tools/list ends up empty at request time, even though the
api-platform loader can produce the full set when invoked directly. Reproduced with a
throwaway command (Symfony console, kernel fully booted):

  • Running the mcp.loader-tagged loader directly into a fresh
    Mcp\Capability\Registry registers all tools:
    $reg = new \Mcp\Capability\Registry();
    foreach ($taggedMcpLoaders as $l) { $l->load($reg); }   // → all tools registered
  • But the registry the built mcp.server actually serves from is empty:
    // reflect mcp.server → protocol → requestHandlers → ListToolsHandler::$registry
    // → 0 tools  (same instance shared with CallToolHandler)
  • mcp.server.builder does contain the api-platform loader
    (ApiPlatform\Mcp\Capability\Registry\Loader), and mcp.server / mcp.registry
    are shared services. So when Builder::build() runs the loaders at the point
    mcp.server is first instantiated, nothing gets registered into the registry the
    server uses — and because the service is shared (built once, especially under
    FrankenPHP worker mode) it stays empty for the rest of the process.

So tools/call keeps working because API Platform's own request handler resolves the
operation from resource metadata directly, independent of the SDK registry; only
tools/list (which reads the SDK registry) is affected.

Workaround

Pin the last good release:

{ "require": { "api-platform/core": "4.3.13" } }

(Exact pin — ^4.3.13 re-resolves to the broken 4.3.14.)

Question

Could you confirm which 4.3.14 change altered MCP tool registration /
Loader wiring (or the build-time/lifecycle ordering of mcp.server)? Glad to provide
a minimal reproducer repo if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions