Skip to content

MCP capability registration via the container

MCP capability registration via the container

INFO

This document represents an architecture decision record (ADR) and has been mirrored from the ADR section in our Shopware 6 repository. You can find the original version here

Context

2026-03-17 - MCP server placement and extensibility settled where MCP capabilities live (core vs plugin vs app) and named the registration and discovery mechanism as public API: mcp.yaml scan_dirs, the DI tags, and McpToolDiscoveryCompilerPass.

The scan_dirs half of that mechanism no longer exists. symfony/mcp-bundle 0.12 replaced the MCP SDK's file-based discovery with compile-time container registration, and 0.13 added support for several MCP servers per application, each declaring which capabilities it exposes. Shopware runs two servers — /api/_mcp and /store-api/_mcp — so the upgrade forced a decision about how a capability reaches the right one.

The placement model from that ADR is unaffected and still stands. Only the registration mechanism is superseded here.

Decision

A capability is registered by its DI tag alone, and assigned to exactly one server.

Registration

Every MCP capability is a container service carrying an MCP attribute (#[McpTool], #[McpPrompt], #[McpResource], #[McpResourceTemplate]) and the matching DI tag (mcp.tool, mcp.prompt, mcp.resource, mcp.resource_template). The bundle collects those at compile time, derives the input schema, and registers them on a server builder. No directory is scanned, so the discovery.scan_dirs option is gone and a capability's location on disk no longer matters.

The Shopware-owned tags stay exactly as they were: plugins and third-party bundles use shopware.mcp.tool / .prompt / .resource, Store API capabilities use shopware.store_api_mcp.*, and McpToolDiscoveryCompilerPass re-tags them. This ADR changes nothing for extension authors.

Server assignment

Each server lists the capabilities it exposes in packages/mcp.php under mcp.servers.<name>.registry, as namespace prefixes:

  • adminShopware\Core\Framework\Mcp\, plus Shopware\Storefront\Mcp\ when that bundle is installed
  • store_apiShopware\Core\System\SalesChannel\Mcp\

Prefixes cannot express plugin or third-party bundle capabilities, whose namespace is arbitrary, and the * wildcard is not usable because it would also claim the other server's capabilities. Those are therefore assigned explicitly: McpToolDiscoveryCompilerPass appends the class name to the bundle's mcp.servers.elements parameter — the channel the bundle's own compiler pass reads its per-server lists from — before that pass runs. Plugin capabilities go to the Admin API server, matching where they were registered before.

A capability assigned to no server is silently not registered. bin/console debug:mcp --native reports those, so the failure mode is diagnosable.

Store API tools carry mcp.tool in addition to shopware.store_api_mcp.tool, because the bundle only collects the SDK tags. The Store API namespace prefix keeps them off the Admin API server, and shopware.store_api_mcp.tool remains the scope marker the analysis passes read.

What stays out of the bundle's hands

Three things the servers configuration cannot express are applied by McpServerBuilderCompilerPass:

  • Protocol handlers are scoped per server. The bundle wires addRequestHandlers() from one global tag for every server, but Shopware's handlers are bound to one registry — both McpAllowlistListRequestHandler instances answer a ListToolsRequest, so a shared tag would let the Admin API handler answer on the Store API endpoint. The tags are mcp.admin.request_handler and mcp.store_api.request_handler.
  • Capability loaders stay on the Admin API server. App capabilities have always been an Admin API concern; the bundle's global addLoaders() would newly advertise every app tool on /store-api/_mcp.
  • Both servers page with shopware.mcp.pagination_limit, so the number the allowlist request handlers slice with cannot drift from the one the SDK advertises.

Consequences

  • An in-tree bundle capability needs one thing instead of two: the DI tag. The old failure mode of a correctly tagged tool silently missing because its directory was not in scan_dirs is gone.
  • It is replaced by a narrower one: a capability in a namespace no server names is not registered. That is what debug:mcp --native exists to surface, and it only affects core and in-tree bundles, since plugin capabilities are assigned explicitly.
  • Moving a core capability to a namespace outside the configured prefixes now requires updating packages/mcp.php. Adding a namespace there is cheap; a prefix that matches nothing is a fatal container error in the bundle, so it cannot rot unnoticed.
  • Shopware depends on the bundle's mcp.servers.elements parameter, which is bundle-internal. It is the mechanism the bundle itself uses to hand per-server lists to its compiler pass, and there is no public alternative for arbitrary-namespace capabilities. The coupling is a known cost of keeping plugin extensibility, and it is covered by unit tests on the assignment.
  • Both servers own separate session stores, because session IDs are not namespaced per server: a shared store would make a session minted on one endpoint valid on the other. Consequently anything reading session liveness across endpoints — McpToolsetSessionCleanupTaskHandler — has to consult every store.
Was this page helpful?
UnsatisfiedSatisfied
Be the first to vote!
0.0 / 5  (0 votes)