diff --git a/.wordlist.txt b/.wordlist.txt index baccf438d1..4ce673a23a 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -760,6 +760,7 @@ PHPUnit PHPunit PII PIM +PKCE POC POS PRs @@ -1341,6 +1342,7 @@ checksums ci classmap classname +claude cleanUp cleanUpPreviousState clearTypeAndCheck diff --git a/guides/plugins/plugins/mcp-server.md b/guides/plugins/plugins/mcp-server.md index d217963f18..ecef6503d2 100644 --- a/guides/plugins/plugins/mcp-server.md +++ b/guides/plugins/plugins/mcp-server.md @@ -76,6 +76,10 @@ class MyTool extends McpToolResponse ) { } + /** + * @param string $email the customer's email address + * @param int $limit how many orders to return, newest first + */ public function __invoke(string $email, int $limit = 10): string { $context = $this->contextProvider->getContext(); @@ -103,6 +107,7 @@ class MyTool extends McpToolResponse - `title` is optional. When set, MCP clients (Claude Desktop, Cursor, etc.) display it in their tool list instead of the machine-readable `name`. Omit it if you have no better label to offer. - Names must only contain `a-zA-Z0-9_-`. - Parameter types on `__invoke()` are mapped to JSON schema. Supported: `string`, `int`, `float`, `bool`. Default values make parameters optional. +- Describe each parameter with a `@param` docblock on `__invoke()`. The MCP SDK copies the text into the parameter's `description` in `inputSchema`. - Obtain the request context via `McpContextProvider::getContext()` injected through the constructor. Do not add a `Context` parameter to `__invoke()`. The MCP SDK does not inject it there. - `requirePrivilege()` returns an error string on failure; check its return value with `if ($error = $this->requirePrivilege(...)) { return $error; }`. `#[McpToolRequires]` is declarative only; without this call there is no runtime enforcement. - Never use `Context::createDefaultContext()` inside a tool. It bypasses the integration's ACL. Use `McpContextProvider::getContext()` instead. @@ -179,7 +184,7 @@ return static function (ContainerConfigurator $configurator): void { }; ``` -Plugin tools use `shopware.mcp.tool` (not `mcp.tool`). The MCP compiler remaps this tag to `mcp.tool` at compile time and registers the tool with the MCP server builder. You do not need a `shopware.feature` flag tag. +Plugin tools use `shopware.mcp.tool` (not `mcp.tool`). The MCP compiler remaps this tag to `mcp.tool` at compile time and assigns the tool to the Admin API server (`/api/_mcp`). You do not need a `shopware.feature` flag tag, and you do not need to list your plugin's namespace or directory in any MCP configuration. ### Available tags @@ -209,6 +214,8 @@ If the tool appears here, it is registered in the live HTTP endpoint and can be - Service is tagged with `shopware.mcp.tool` - `#[McpTool]` is on the class, not on `__invoke()` +Since Shopware 6.7.15.0, `debug:mcp` lists tools only. Use `bin/console debug:mcp --native` to verify prompts and resources. + ## Adding prompts Follow the same pattern with `#[McpPrompt]` and `shopware.mcp.prompt`: diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index 604d265f39..1407863f15 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -13,6 +13,8 @@ This page describes Shopware 6.7.14.0 and later, where the MCP server is always On Shopware 6.7.11.0 to 6.7.13.x, the MCP server is gated behind the `MCP_SERVER` feature flag. Set `MCP_SERVER=1` in your `.env` file to enable the endpoint. Those versions advertise every allowed tool in `tools/list`; the discovery tools, tool groups, toolsets, cursor pagination, and `listChanged` notifications do not exist there. Starting with 6.7.14.0, the flag is removed and has no effect. Remove `MCP_SERVER` from your `.env` file. The MCP classes stay marked as experimental until 6.8.0. + +Starting with 6.7.15.0, Shopware runs on `symfony/mcp-bundle` 0.13. This changes the list page size setting, the session store configuration, the `debug:mcp` command, and some internal service IDs. See [Upgrading to 6.7.15.0](#upgrading-to-6-7-15-0). ::: ## Shopware MCP configuration @@ -33,7 +35,7 @@ These two keys are the complete `shopware.mcp` configuration: | `allowed_tools` | list of strings | `[]` | Installation-wide tool allowlist applied at compile time. Empty = all. | | `app_tool_timeout` | integer | `10` | Timeout in seconds for app webhook tool calls. Minimum `1`. | -Everything else — the endpoint path, server instructions, and list pagination — is configured on the `symfony/mcp-bundle` extension, not under `shopware.mcp`. +The servers, their instructions, and their session stores are configured on the `symfony/mcp-bundle` extension, not under `shopware.mcp`. The list page size is the `shopware.mcp.pagination_limit` container parameter, see [Capability list pagination](#capability-list-pagination). ### Global tool allowlist @@ -63,6 +65,7 @@ Shopware applies a per-principal MCP allowlist depending on how the client authe | Integration access key (`SWIA...`) | Per-integration allowlist under **Settings → Integrations → Edit MCP Allowlist** | | User access key (`SWUA...`) | Per-user allowlist under **Settings → Users & Permissions → [user] → MCP tool allowlist** | | Bearer JWT, password / refresh grant | Per-user allowlist of the authenticated user | +| Bearer JWT, authorization code (PKCE) | Per-user allowlist of the user who approved the request | | Bearer JWT, client_credentials | Per-integration allowlist | | Integration + `sw-app-user-id` (Copilot) | Intersection of the integration allowlist and the user allowlist | @@ -106,45 +109,78 @@ This pattern lets the app owner control which tools the integration may ever cal ## MCP bundle configuration -The underlying `symfony/mcp-bundle` is configured in `config/packages/mcp.php`. Shopware ships this file, and Symfony loads it automatically. You do not need to create or modify it for standard setups. +Shopware ships the configuration of the underlying `symfony/mcp-bundle`, and Symfony loads it automatically. You do not need to create or modify it for standard setups. + +Shopware has served both MCP endpoints since 6.7.11.0. Since 6.7.15.0, the configuration declares them as two servers under the bundle's `servers` key. Before that, Shopware registered the Store API server with its own services outside the bundle configuration. The following table lists the two servers: + +| Server | Endpoint | Exposes | +| ----------- | ----------------- | ------------------------------------------------------------------------ | +| `admin` | `/api/_mcp` | Core, Storefront, plugin, bundle, and app capabilities for the Admin API | +| `store_api` | `/store-api/_mcp` | Capabilities tagged with `shopware.store_api_mcp.*` | + +For both servers, the bundle's HTTP and stdio transports are switched off (`transports.http: false`), because Shopware's own controllers serve the endpoints. -This file also sets the server `instructions` that clients receive during `initialize`. They tell the agent that the advertised tool list is not the full catalogue and that it should call `shopware-tool-search` before concluding that an action is unsupported. +The configuration also sets the server `instructions` that clients receive during `initialize`. They tell the agent that the advertised tool list is not the full catalogue and that it should call `shopware-tool-search` before concluding that an action is unsupported. + +You can add settings for a server in your own `config/packages/mcp.yaml`. Symfony merges them with the values shipped by Shopware. Use this for the [session store](#session-store). Do not change `transports` or `registry`, because Shopware relies on them to apply authentication and to assign capabilities. ## Capability list pagination The `tools/list`, `resources/list`, and `prompts/list` methods use MCP cursor pagination. When a response contains `nextCursor`, pass that value unchanged as `cursor` in the next request. Continue until `nextCursor` is absent. -The default page size is 50 entries. It is the MCP bundle's `pagination_limit` option, not a `shopware.mcp` key. Shopware does not set it, so to change it, configure the `mcp` extension in your own config file: +The default page size is 50 entries. Since Shopware 6.7.15.0, it is the `shopware.mcp.pagination_limit` container parameter, which applies to both servers. To change it, override the parameter: ```yaml -# config/packages/mcp.yaml -mcp: - pagination_limit: 100 +# config/services.yaml +parameters: + shopware.mcp.pagination_limit: 100 ``` +On 6.7.14.x, the page size is the MCP bundle's `pagination_limit` option instead. Configure it with `mcp.pagination_limit` in `config/packages/mcp.yaml`. The bundle removed that option in 0.13. + Treat cursors as opaque values. Shopware applies the effective allowlist before pagination, so each page contains only capabilities the current principal may access, and a cursor is only meaningful for the principal that received it. An unknown, malformed, or out-of-range cursor is answered with the JSON-RPC error `-32602` and the message `Invalid value for pagination parameter "cursor"`. -In practice `tools/list` rarely paginates: it only contains the discovery tools plus the tools of the toolsets enabled for the current session. `resources/templates/list` is not allowlist-filtered. +In practice `tools/list` rarely paginates: it only contains the discovery tools plus the tools of the toolsets enabled for the current session or requested with [`?toolsets`](./getting-started.md#select-toolsets-when-connecting). A client that connects with `?toolsets=all` and ignores `nextCursor` sees only the first page. `resources/templates/list` is not allowlist-filtered. ## Session store MCP sessions track an ongoing conversation across multiple requests. The client performs an `initialize` handshake first, then sends subsequent `tools/call` requests referencing that session ID. Session data and enabled toolsets must survive between requests. -Shopware defaults to a file-based session store that writes to `%kernel.cache_dir%/mcp-sessions/`. +Each MCP server has its own session store. Session IDs are not namespaced per server, so a shared store would make a session created on `/api/_mcp` valid on `/store-api/_mcp`. The bundle refuses to build the container when two servers share the same storage location. -Enabled toolsets are stored separately, in the `mcp_toolset_session` database table, keyed on the `Mcp-Session-Id` header only — not per user and not per integration. Rows are deleted when the client ends the session with `DELETE /api/_mcp`. Sessions that are abandoned without a `DELETE` are cleaned up by the daily `mcp_toolset_session.cleanup` scheduled task, so the scheduler must run in production. +Shopware defaults to a file-based session store per server, which writes to `%kernel.cache_dir%/mcp-sessions/`, for example `mcp-sessions/admin` and `mcp-sessions/store_api`. Before 6.7.15.0, both endpoints wrote to `%kernel.cache_dir%/mcp-sessions/`. Sessions that existed before the update are not carried over, on either endpoint, so every MCP client initializes once more. -| Store | Multi-worker | Multi-server | Backend | -| ----------------------------------------------- | ------------ | ------------ | ----------------------------------------------------------------------------- | -| `file` (default) | No | No | `%kernel.cache_dir%/mcp-sessions/` | -| `memory` | No | No | Per-process RAM | -| `cache` (avoid) | No in dev | No | `cache.app` (ArrayAdapter in dev) | -| `framework` (unusable in Shopware) | Yes | Yes | Requires active PHP session, not available because the Admin API is stateless | -| Custom Redis store (recommended for production) | Yes | Yes | Redis / Valkey | +Enabled toolsets are stored separately, in the `mcp_toolset_session` database table, keyed on the `Mcp-Session-Id` header only, not per user and not per integration. Rows are deleted when the client ends the session with `DELETE /api/_mcp` or `DELETE /store-api/_mcp`. Sessions that are abandoned without a `DELETE` are cleaned up by the daily `mcp_toolset_session.cleanup` scheduled task, which checks the session stores of both servers. The scheduler must run in production. + +The `session` options of each server select the store: + +| `store` | Multi-worker | Multi-server | Backend | +| ---------------------------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------------------ | +| `file` (default) | No | No | `session.directory`, default `%kernel.cache_dir%/mcp-sessions/` | +| `memory` | No | No | Per-process RAM | +| `cache` | Yes, with a shared pool | Yes, with a shared pool | The PSR-16 service in `session.cache_pool`. The default pool wraps `cache.app` | +| `framework` (unusable in Shopware) | Yes | Yes | Requires active PHP session, not available because the Admin API is stateless | + +The `cache` store is only as shared as its pool. Point `cache_pool` at a Redis-backed pool for production. The `cache` and `framework` stores prefix keys with `mcp--` by default, so both servers can use the same pool. Every store expires sessions after `session.ttl` seconds (default `3600`). ### Production: Redis session store -The file store works on a single machine. In a multi-server or Kubernetes environment, `initialize` and subsequent tool calls may land on different workers that do not share a local filesystem. Switch to Redis: +The file store works on a single machine. In a multi-server or Kubernetes environment, `initialize` and subsequent tool calls may land on different workers that do not share a local filesystem. Switch both servers to a Redis-backed cache store: + +**`config/packages/mcp.yaml`:** + +```yaml +mcp: + servers: + admin: + session: + store: cache + cache_pool: mcp.session.cache_psr16 + store_api: + session: + store: cache + cache_pool: mcp.session.cache_psr16 +``` **`config/services.yaml`:** @@ -153,12 +189,6 @@ services: mcp.session.cache_psr16: class: Symfony\Component\Cache\Psr16Cache arguments: ['@cache.mcp_sessions'] - - mcp.session.store: - class: Mcp\Server\Session\Psr16SessionStore - arguments: - - '@mcp.session.cache_psr16' - - 3600 # TTL in seconds ``` **`config/packages/framework.yaml`:** @@ -175,6 +205,36 @@ framework: If you already have a Redis/Valkey connection configured for Shopware, set `provider` to the same DSN to avoid opening a second connection. +A configuration that is not picked up leaves the file store in place, and the endpoints keep working. To confirm the switch, inspect the store of each server: + +```bash +bin/console debug:container mcp.server.admin.session.store --show-arguments +bin/console debug:container mcp.server.store_api.session.store --show-arguments +``` + +Both must report the class `Mcp\Server\Session\Psr16SessionStore`, with the prefixes `mcp-admin-` and `mcp-store_api-`. Switching the store discards the sessions that exist at that moment, so connected clients initialize again. + +#### Active session registry + +When an app with MCP capabilities is installed, updated, activated, deactivated, or deleted, Shopware sends `notifications/tools/list_changed` to every active session of both servers. It finds those sessions in an active session registry, which it keeps in `cache.system`. That cache is local to each server, so on several servers the notification only reaches sessions that were initialized on the server that processed the app change. Point both registries at the shared pool as well: + +**`config/services.yaml`:** + +```yaml +services: + shopware.mcp.session_registry_cache: + class: Symfony\Component\Cache\Psr16Cache + arguments: ['@cache.mcp_sessions'] + + mcp.store_api.session_registry_cache: + class: Symfony\Component\Cache\Psr16Cache + arguments: ['@cache.mcp_sessions'] +``` + +The two registries use different cache keys, so they can share the pool with the session stores. A registry is written without a TTL, so it expires after the pool's `default_lifetime`. Shopware rewrites it on every MCP request, so it outlives its sessions as long as `default_lifetime` is not shorter than `session.ttl` or is left unset. They serialize their updates with a lock from `lock.factory`. Configure a shared [lock store](../../../guides/hosting/performance/lock-store.md) when you run more than one server. These service IDs are the same on 6.7.14.x. + +On 6.7.14.x, the bundle has no per-server session options. There, override the `mcp.session.store` service with a `Mcp\Server\Session\Psr16SessionStore` that receives `@mcp.session.cache_psr16` and a TTL. Remove that override when you update to 6.7.15.0, because Shopware no longer uses the `mcp.session.store` service. + ## ACL and permissions All MCP tool operations respect the integration's Admin API ACL role. To restrict what an MCP client can do: @@ -195,7 +255,7 @@ The Admin UI surfaces two helpers for getting ACL right: ## CLI: `debug:mcp` -List all registered capabilities: +List all registered tools of both servers: ```bash bin/console debug:mcp @@ -203,16 +263,23 @@ bin/console debug:mcp The tool output shows five columns: **Name**, **Group**, **Source**, **Dependencies**, and **Privileges**. It reads from the complete live server registry and covers core and extension tools in one view. The **Group** becomes the toolset name used for progressive discovery. -The command prints separate sections for **Tools**, **Prompts**, **Resources**, and **Resource Templates**. It covers the Admin API server only; Store API capabilities are not listed — see [Store API MCP](./store-api.md). +The command prints one section per server. Limit the output to one server with `--scope`: -Filter by capability type: +```bash +bin/console debug:mcp --scope=api # /api/_mcp only +bin/console debug:mcp --scope=store-api # /store-api/_mcp only +``` + +Since Shopware 6.7.15.0, `debug:mcp` lists tools only. `symfony/mcp-bundle` ships its own `debug:mcp` command, which Shopware renames to `debug:mcp:native`. Use it for prompts, resources, resource templates, and the configured servers: ```bash -bin/console debug:mcp --tools # tools only -bin/console debug:mcp --prompts # prompts only -bin/console debug:mcp --resources # resources only +bin/console debug:mcp --native # same as bin/console debug:mcp:native ``` +The `--tools` option is still accepted but has no effect. The `--prompts` and `--resources` options were removed in 6.7.15.0. + +If a capability carries an MCP attribute but no server exposes it, `debug:mcp` prints a warning below the tool list. Such a capability is unreachable on both endpoints. + Drill into a single capability by name: ```bash @@ -225,6 +292,8 @@ See the registry from a specific integration's perspective (honors its per-integ bin/console debug:mcp --integration=SWIA... ``` +The integration filter applies to the Admin API server only. Store API tools are listed unfiltered. + If a tool is missing from this output, it is also missing from the live endpoint. Common causes: - Plugin is not installed or activated @@ -242,3 +311,42 @@ Both MCP endpoints are rate limited with their own buckets, configured under `sh | `mcp_store_api` | `/store-api/_mcp` | Sales channel and context token, plus client IP | 120 per minute, 600 per 10 minutes | Both use the `time_backoff` policy and reset after one hour. Exceeding a limit returns HTTP 429 with the remaining wait time in the response body; no `Retry-After` header is sent. + +## Upgrading to 6.7.15.0 + +Shopware 6.7.15.0 updates `symfony/mcp-bundle` to 0.13 and `mcp/sdk` to 0.8. Plugins and apps that register tools, prompts, or resources through the `shopware.mcp.*` or `shopware.store_api_mcp.*` tags or through `Resources/mcp.xml` need no changes. + +Code that integrates with the MCP internals must be updated. These classes and services are marked `@experimental stableVersion:v6.8.0`. + +The bundle registers one set of services per server, so the flat service IDs were renamed: + +| Before | After | +| ------------------------------ | ----------------------------------------------------------------------------------------- | +| `mcp.server` | `mcp.server.admin` | +| `mcp.server.builder` | `mcp.server.admin.builder` | +| `mcp.registry` | `mcp.server.admin.registry` | +| `mcp.store_api.server` | `mcp.server.store_api` | +| `mcp.store_api.server.builder` | `mcp.server.store_api.builder` | +| `mcp.store_api.registry` | `mcp.server.store_api.registry` | +| `mcp.session.store` | `mcp.server.admin.session.store` and `mcp.server.store_api.session.store` (shared before) | + +The `StoreApiMcpServerBuilderCompilerPass` was removed. + +Protocol request and notification handlers are scoped per server: + +| Target server | Request handler tag | Notification handler tag | +| ------------- | ------------------------------- | ------------------------------------ | +| Admin API | `mcp.admin.request_handler` | `mcp.admin.notification_handler` | +| Store API | `mcp.store_api.request_handler` | `mcp.store_api.notification_handler` | + +A handler that only carries the bundle's global `mcp.request_handler` or `mcp.notification_handler` tag reaches neither server. + +Further changes: + +- The `discovery.scan_dirs` option was removed. Capabilities are registered from their service tag at compile time. Each server lists the namespace prefixes it exposes under `mcp.servers..registry`, and Shopware assigns plugin and third-party bundle capabilities automatically. +- The bundle's `mcp.pagination_limit` option was removed. Use the `shopware.mcp.pagination_limit` parameter, see [Capability list pagination](#capability-list-pagination). +- Each server has its own session store. A custom `mcp.session.store` service is no longer used, see [Session store](#session-store). +- `debug:mcp` lists tools only. Use `debug:mcp --native` for prompts and resources. +- Both endpoints keep serving the protocol revisions they served before, so the negotiated `protocolVersion` and the `Mcp-Session-Id` behavior are unchanged. + +For the reasoning behind these changes, see the ADR [MCP capability registration via the container](../../../resources/references/adr/2026-08-31-mcp-capability-registration-via-container.md). diff --git a/products/tools/mcp-server/getting-started.md b/products/tools/mcp-server/getting-started.md index c1e33a0ac4..366588fcb1 100644 --- a/products/tools/mcp-server/getting-started.md +++ b/products/tools/mcp-server/getting-started.md @@ -11,7 +11,7 @@ This guide walks you through connecting an AI client to a Shopware shop using th ## Prerequisites -- Shopware 6.7.14.0 or later. On 6.7.11.0 to 6.7.13.x, set `MCP_SERVER=1` in your `.env` file first and skip the discovery section below — see [Configuration](./configuration.md). +- Shopware 6.7.14.0 or later. On 6.7.11.0 to 6.7.13.x, set `MCP_SERVER=1` in your `.env` file first and skip the discovery section below. See [Configuration](./configuration.md). Selecting toolsets in the connection URL requires 6.7.15.0 or later. - `symfony/mcp-bundle` installed — verify with `composer show symfony/mcp-bundle`. If it is missing, ensure it is listed as a dependency in `composer.json` and run `composer install`. ## Step 1: Create an integration @@ -124,7 +124,7 @@ Verify the server is working with the CLI: bin/console debug:mcp ``` -This lists the complete registered capability catalogue. A fresh AI client session initially receives only the discovery tools described in the following section. +This lists every registered tool of both MCP servers. Use `bin/console debug:mcp --native` to list prompts and resources. A fresh AI client session initially receives only the discovery tools described in the following section. ## Discover tools on demand @@ -147,6 +147,27 @@ Enabled toolsets remain active only for the current MCP session and are tracked The toolset names shipped by core are listed in the [Tools Reference](./tools-reference.md#toolsets). +### Select toolsets when connecting + +Some clients read `tools/list` only once per connection and ignore `notifications/tools/list_changed`. claude.ai is one of them. In these clients, a toolset enabled during the conversation never becomes visible. + +Since Shopware 6.7.15.0, you can name toolsets in the URL the client connects to. Their tools are then part of the first `tools/list`: + +```text +https://your-shop.example.com/api/_mcp?toolsets=order,media +https://your-shop.example.com/api/_mcp?toolsets=all +``` + +- Separate toolset names with commas. `all` selects every toolset the principal may see. +- Unknown names are ignored. A malformed parameter never makes `tools/list` fail. +- The selection is read from every request and is not stored. No session is required, and nothing expires. +- The parameter only changes which tools are advertised. The MCP allowlist and the ACL role still decide what may be called, so a toolset outside the allowlist stays hidden. +- Without the parameter, the endpoint behaves as before. + +The Store API endpoint accepts the same parameter, for example `/store-api/_mcp?toolsets=store-api`. + +Prefer a short list of focused toolsets over `all`. Every advertised tool uses space in the agent's context window, and `tools/list` is paginated with 50 entries per page. A client that ignores `nextCursor` sees only the first page. + ## Authentication methods ### Integration credentials (recommended) @@ -157,6 +178,8 @@ Pass `sw-access-key` and `sw-secret-access-key` as HTTP headers. Credentials are Standard Admin API OAuth bearer tokens also work. Obtain one via the `/api/oauth/token` endpoint. Tokens expire (default: 10 minutes), so integration credentials are preferred for persistent MCP clients. When authenticated via bearer token, the user's per-user allowlist applies (configured under **Settings → Users & Permissions → [user] → MCP tool allowlist**). +A missing, invalid, or expired token is answered with HTTP 401 and the JSON-RPC error code `-32001`. The message includes the reason, such as `Access token is expired`, so the client can refresh the token. Before Shopware 6.7.15.0, `/api/_mcp` answered these cases with HTTP 500 and `-32000`. + ## Controlling which capabilities are available By default, an admin integration is permitted to call all registered tools, resources, and prompts. Only the discovery tools are advertised at the start of a session. To restrict which capabilities can be discovered and called: diff --git a/products/tools/mcp-server/index.md b/products/tools/mcp-server/index.md index d9b83b6ce0..8a7f1a4f08 100644 --- a/products/tools/mcp-server/index.md +++ b/products/tools/mcp-server/index.md @@ -159,6 +159,7 @@ The Shopware MCP Server is currently available as an experimental feature. :::info Introduced in Shopware 6.7.11.0 Progressive tool discovery and the removal of the `MCP_SERVER` feature flag in Shopware 6.7.14.0 +Connect-time toolset selection and `symfony/mcp-bundle` 0.13 in Shopware 6.7.15.0 Based on the MCP specification Available through the `/api/_mcp` and `/store-api/_mcp` endpoints ::: diff --git a/products/tools/mcp-server/intro.md b/products/tools/mcp-server/intro.md index 5ba316d14d..3ebd9a1085 100644 --- a/products/tools/mcp-server/intro.md +++ b/products/tools/mcp-server/intro.md @@ -14,7 +14,7 @@ Shopware ships a native MCP server with the core platform. It exposes an endpoin :::info Experimental The MCP server is considered experimental until Shopware 6.8. APIs and tool names may change before the stable release. -The MCP server was introduced in Shopware 6.7.11.0 behind the `MCP_SERVER` feature flag. Shopware 6.7.14.0 removes the flag and introduces progressive tool discovery — see [Configuration](./configuration.md) for what applies to which version. +The MCP server was introduced in Shopware 6.7.11.0 behind the `MCP_SERVER` feature flag. Shopware 6.7.14.0 removes the flag and introduces progressive tool discovery. Shopware 6.7.15.0 moves to `symfony/mcp-bundle` 0.13 and adds connect-time toolset selection. See [Configuration](./configuration.md) for what applies to which version. ::: ## What the MCP server provides @@ -28,7 +28,7 @@ The following table summarizes the MCP server's core capabilities. | **Authorization** | Admin API ACL enforcement or the current Store API sales-channel context | | **Tool allowlist** | Per-integration and per-user selection in Admin UI; intersected when an app forwards `sw-app-user-id` | | **Rate limiting** | Per-principal rate limiting | -| **Discovery** | A small default tool surface with search and session-scoped toolsets | +| **Discovery** | A small default tool surface with search, session-scoped toolsets, and connect-time `?toolsets` | | **Extensibility** | Extensions can contribute custom tools, prompts, and resources | ## Architecture @@ -39,6 +39,14 @@ The following table summarizes the MCP server's core capabilities. **Apps** register capabilities declaratively in `Resources/mcp.xml`. Shopware calls the app's endpoint over HTTP at runtime, using an HMAC-signed request. Use an app when your logic runs on a remote service, requires cloud compatibility, or should be deployed independently from Shopware. +### Two servers, one bundle + +Shopware runs two MCP servers side by side: `admin` behind `/api/_mcp` and `store_api` behind `/store-api/_mcp`. Both have existed since Shopware 6.7.11.0. Up to 6.7.14.x, Shopware built the Store API server by hand next to the bundle's single server. Since 6.7.15.0, both are declared under the `servers` key of the `symfony/mcp-bundle` configuration. Each server has its own registry, its own session store, and its own set of protocol handlers. A session created on one endpoint is therefore not valid on the other. + +The bundle's own HTTP transport is switched off for both servers. Shopware's controllers own the endpoints, so authentication, rate limiting, and the capability allowlist always run before a request reaches the bundle. + +Capabilities are registered at container compile time from their service tags. Core capabilities are assigned to a server by namespace prefix. Plugin and third-party bundle capabilities are assigned by Shopware based on the tag they carry: `shopware.mcp.*` goes to the Admin API server, and `shopware.store_api_mcp.*` goes to the Store API server. App capabilities are loaded at runtime and are only served on the Admin API server. + ### Plugin or App? **Use a plugin or Symfony bundle when:** @@ -55,7 +63,9 @@ The following table summarizes the MCP server's core capabilities. ## Spec coverage and known limitations -Shopware's MCP server is built on `symfony/mcp-bundle` (currently `~0.11.0` with `mcp/sdk ^0.7.0`) and takes the [MCP specification (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25) as its reference. Shopware does not pin a protocol revision: the SDK negotiates it from the client's `initialize` request. The bundle may not cover every feature of the reference revision, and some areas of the spec are only partially implemented. +Shopware's MCP server is built on `symfony/mcp-bundle` and takes the [MCP specification (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25) as its reference. Shopware 6.7.15.0 and later use `symfony/mcp-bundle ~0.13.0` with `mcp/sdk ^0.8.1`. Shopware 6.7.14.x uses `~0.11.0` with `mcp/sdk ^0.7.0`. + +Shopware serves only the session-based handshake protocol era. The SDK negotiates the concrete revision from the client's `initialize` request within that era. `mcp/sdk` 0.8 also supports the stateless 2026-07-28 revision, but Shopware disables it on both endpoints. That revision issues no `Mcp-Session-Id`, and session-scoped toolsets and `listChanged` notifications depend on that header. The bundle may not cover every feature of the reference revision, and some areas of the spec are only partially implemented. Shopware supports cursor pagination for capability lists and emits `listChanged` notifications when capabilities change. The following table lists the remaining known gaps. diff --git a/products/tools/mcp-server/store-api.md b/products/tools/mcp-server/store-api.md index 72c5de9256..1e14cce142 100644 --- a/products/tools/mcp-server/store-api.md +++ b/products/tools/mcp-server/store-api.md @@ -53,6 +53,10 @@ The Store API server uses the same progressive discovery flow as the Admin API s Since Shopware 6.7.14.0, a bare `tools/list` on `/store-api/_mcp` no longer returns the full catalogue. Even `shopware-store-api-context` is deferred behind the `store-api` toolset. Clients that expected the complete list must either follow the discovery flow or enable the toolset explicitly. ::: +Since Shopware 6.7.15.0, clients can also select toolsets in the connection URL, for example `/store-api/_mcp?toolsets=store-api`. The tools of the named toolsets are part of the first `tools/list`, without a `shopware-toolset-enable` call. See [Select toolsets when connecting](./getting-started.md#select-toolsets-when-connecting). + +The Store API server keeps its sessions in its own session store, separate from the Admin API server. An `Mcp-Session-Id` from `/api/_mcp` is not valid on `/store-api/_mcp`. See [Session store](./configuration.md#session-store). + The server advertises `instructions` during `initialize` that state the same thing: > This MCP server exposes Store API capabilities. All operations run in the current sales-channel context and use Store API authentication headers. The advertised tool list is not the full catalogue: if no advertised tool matches the requested action, call shopware-tool-search first instead of assuming the action is unsupported, then use shopware-toolsets-list and shopware-toolset-enable to make a matched tool callable if your client cannot invoke it inline. @@ -91,7 +95,7 @@ Core registers no prompts or resources for the Store API scope. ## Sessions and rate limiting -MCP sessions and enabled toolsets use the same storage as the Admin API server, including the `mcp_toolset_session` table and its daily cleanup task — see [Session store](./configuration.md#session-store). Toolset names are not namespaced per endpoint. +MCP sessions live in the Store API server's own session store. Enabled toolsets are stored in the `mcp_toolset_session` table, which both endpoints share, including its daily cleanup task. See [Session store](./configuration.md#session-store). Toolset names are not namespaced per endpoint. Rate limiting uses its own bucket, `mcp_store_api`: 120 requests per minute and 600 per 10 minutes, keyed on the sales channel and context token, plus an additional per-client-IP limit. The limits are deliberately tighter than the Admin API's, because a context token is cheap to rotate. See [Rate limiting](./configuration.md#rate-limiting). @@ -133,4 +137,5 @@ Notes for extension developers: - Use `StoreApiMcpContextProvider` to obtain the sales-channel context. There is no Admin API context and no `requirePrivilege()` equivalent. - Assign a group with `#[McpToolGroup]` so clients can discover and enable your tools — the same grouping rules apply as for the Admin API. See [Assign a tool group](../../../guides/plugins/plugins/mcp-server.md#step-2-assign-a-tool-group). - Registering the same tool name for both scopes requires two classes. The MCP SDK binds a tool to the class that carries the `#[McpTool]` attribute, which is why core has separate Admin API and Store API discovery tool classes. -- `bin/console debug:mcp` covers the Admin API server only. Store API capabilities do not appear in its output. +- `bin/console debug:mcp` lists the tools of both servers. Use `bin/console debug:mcp --scope=store-api` to show only the Store API server, and `bin/console debug:mcp --native` for prompts and resources. +- Capabilities are assigned to the Store API server by their `shopware.store_api_mcp.*` tag. No namespace or directory configuration is required. diff --git a/products/tools/mcp-server/tools-reference.md b/products/tools/mcp-server/tools-reference.md index 28b9fc2aa2..fd7085cdf6 100644 --- a/products/tools/mcp-server/tools-reference.md +++ b/products/tools/mcp-server/tools-reference.md @@ -64,6 +64,8 @@ Enable one toolset per call. There is no counterpart for disabling a toolset, an The response sets `_meta.listChanged` to `true` and Shopware emits `notifications/tools/list_changed`. Clients that do not refresh automatically must request `tools/list` again. Enabling a toolset does not grant permission to call its tools; the effective MCP allowlist remains the security boundary. +For clients that never refresh `tools/list`, name the toolsets in the connection URL instead, for example `/api/_mcp?toolsets=entity,order`. See [Select toolsets when connecting](./getting-started.md#select-toolsets-when-connecting). + ## Toolsets Every tool belongs to exactly one group, and every group except `discovery` becomes a toolset that a client can enable. Core ships these toolsets: @@ -171,11 +173,11 @@ Use this instead of `shopware-entity-search` when you need counts, averages, sum **Parameters:** -| Name | Type | Required | Default | Description | -| -------------- | ------ | -------- | ------- | ------------------------------------- | -| `entity` | string | yes | — | Entity name | -| `aggregations` | string | yes | — | JSON array of aggregation definitions | -| `filters` | string | no | `[]` | JSON array of filter definitions | +| Name | Type | Required | Default | Description | +| -------------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------- | +| `entity` | string | yes | — | Entity name | +| `aggregations` | string | yes | — | JSON array of aggregation definitions. A single object instead of an array is rejected | +| `filters` | string | no | `[]` | JSON array of filter definitions | **Supported aggregation types:** `avg`, `sum`, `min`, `max`, `count`, `terms`, `date-histogram`, `range`, `filter`, `entity` diff --git a/products/tools/mcp-server/troubleshooting.md b/products/tools/mcp-server/troubleshooting.md index 855207b588..a0c9c1b56d 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,27 +9,31 @@ nav: ## Quick reference -| Symptom | Likely cause | Fix | -| ------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------- | -| `Authentication failed. Configure your MCP client...` | Wrong or missing credentials | Check `sw-access-key` / `sw-secret-access-key` in your client config | -| `Tool "X" is not in the allowlist...` | Tool not enabled for this integration | Settings → Integrations → Edit MCP Allowlist → enable the tool | -| `Resource "X" is not in the allowlist...` | Resource not enabled for this integration | Settings → Integrations → Edit MCP Allowlist → enable the resource | -| `Prompt "X" is not in the allowlist...` | Prompt not enabled for this integration | Settings → Integrations → Edit MCP Allowlist → enable the prompt | -| `Missing privilege: {entity}:read` | Integration role lacks the permission | Assign an ACL role with the required privilege, or use `--admin` | -| Only three tools appear in a fresh session | Expected progressive discovery behavior | Use `shopware-tool-search` or enable a toolset with `shopware-toolset-enable` | -| Allowed tool missing from `tools/list` | Its toolset is not enabled | Search for the tool, enable its toolset, then refresh `tools/list` | -| Enabled toolset does not appear | Client ignored `notifications/tools/list_changed` | Refresh `tools/list` manually | -| No tools appear in `tools/list` | Discovery tools were removed by global `allowed_tools` | Add all three discovery tools to `shopware.mcp.allowed_tools` | -| Admin integration but tool still blocked | Per-integration allowlist is set | Admin bypasses ACL only; the integration allowlist still applies | -| Tool search returns every tool for an admin user | Admin user login bypasses the per-user allowlist | Use a non-admin user when the per-user allowlist must apply | -| `shopware-tool-search` returns nothing | Query too vague; low-scoring matches are dropped | Search with concrete domain wording, or list toolsets with `shopware-toolsets-list` | -| `Cannot enable an MCP toolset without an active MCP session.` | Client did not send the `Mcp-Session-Id` header | Complete the `initialize` handshake and send the returned session ID with every request | -| `Invalid value for pagination parameter "cursor"` | Cursor is stale, malformed, or from another principal | Restart the list from the first page without a `cursor` | -| Toolsets are forgotten between requests | Session store is not shared across workers | Use a Redis session store in multi-server setups | -| Tool missing entirely | Extension inactive or capability registration missing | Check `bin/console debug:mcp` | -| `ECONNREFUSED` or "fetch failed" | Server not running or wrong URL | Start Shopware and verify the URL in your client config | -| Client shows "Needs authentication" after failed connect | Client fell back to `/register` OAuth endpoint | Verify the credentials and ensure the URL ends with `/api/_mcp` | -| A tool returns `shopware://tool-result/...` instead of data | Result exceeded 100 KB and was offloaded | Read the URI as a resource, or narrow the request with a smaller `limit` or fewer fields | +| Symptom | Likely cause | Fix | +| ------------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | +| `Authentication failed. Configure your MCP client...` | Wrong or missing credentials | Check `sw-access-key` / `sw-secret-access-key` in your client config | +| HTTP 401 with JSON-RPC code `-32001` | Bearer token missing, invalid, or expired | Send a bearer token if it is missing, otherwise get a new one; the message names the reason | +| `Tool "X" is not enabled in your MCP allowlist.` | Tool not enabled for this integration | Settings → Integrations → Edit MCP Allowlist → enable the tool | +| `Resource "X" is not enabled in your MCP allowlist.` | Resource not enabled for this integration | Settings → Integrations → Edit MCP Allowlist → enable the resource | +| `Prompt "X" is not enabled in your MCP allowlist.` | Prompt not enabled for this integration | Settings → Integrations → Edit MCP Allowlist → enable the prompt | +| `Missing privilege: {entity}:read` | Integration role lacks the permission | Assign an ACL role with the required privilege, or use `--admin` | +| Only three tools appear in a fresh session | Expected progressive discovery behavior | Use `shopware-tool-search` or enable a toolset with `shopware-toolset-enable` | +| Allowed tool missing from `tools/list` | Its toolset is not enabled | Search for the tool, enable its toolset, then refresh `tools/list` | +| Enabled toolset does not appear | Client ignored `notifications/tools/list_changed` | Refresh `tools/list` manually, or connect with `?toolsets=` | +| Enabled toolset never appears in claude.ai | Client reads `tools/list` once per connection | Add `?toolsets=` to the connection URL (6.7.15.0 and later) | +| No tools appear in `tools/list` | Discovery tools were removed by global `allowed_tools` | Add all three discovery tools to `shopware.mcp.allowed_tools` | +| Admin integration but tool still blocked | Per-integration allowlist is set | Admin bypasses ACL only; the integration allowlist still applies | +| Tool search returns every tool for an admin user | Admin user login bypasses the per-user allowlist | Use a non-admin user when the per-user allowlist must apply | +| `shopware-tool-search` returns nothing | Query too vague; low-scoring matches are dropped | Search with concrete domain wording, or list toolsets with `shopware-toolsets-list` | +| `Cannot enable an MCP toolset without an active MCP session.` | Client did not send the `Mcp-Session-Id` header | Complete the `initialize` handshake and send the returned session ID with every request | +| `Invalid value for pagination parameter "cursor"` | Cursor is stale, malformed, or from another principal | Restart the list from the first page without a `cursor` | +| Toolsets are forgotten between requests | Session store is not shared across workers | Use a Redis session store in multi-server setups | +| Session rejected after updating to 6.7.15.0 | Each server now has its own session store | Send `initialize` again to get a new session | +| Tool missing entirely | Extension inactive or capability registration missing | Check `bin/console debug:mcp` | +| Prompts and resources missing from `debug:mcp` | Since 6.7.15.0, the command lists tools only | Run `bin/console debug:mcp --native` | +| `ECONNREFUSED` or "fetch failed" | Server not running or wrong URL | Start Shopware and verify the URL in your client config | +| Client shows "Needs authentication" after failed connect | Client fell back to `/register` OAuth endpoint | Verify the credentials and ensure the URL ends with `/api/_mcp` | +| A tool returns `shopware://tool-result/...` instead of data | Result exceeded 100 KB and was offloaded | Read the URI as a resource, or narrow the request with a smaller `limit` or fewer fields | ## Connection issues @@ -87,8 +91,17 @@ If a tool does not appear in the `debug:mcp` output, it will also be missing fro **For core/bundle tools:** -- Confirm the directory is listed in `config/packages/mcp.php` `scan_dirs` -- Confirm the service has the correct DI tag (`mcp.tool` for in-tree bundles) +- Confirm the service has the correct DI tag (`shopware.mcp.tool`, or `shopware.store_api_mcp.tool` for the Store API) +- Check the warning below the `debug:mcp` tool list. It names capabilities that carry an MCP attribute but that no server exposes. `bin/console debug:mcp --native` shows the same for prompts and resources. +- On 6.7.14.x and earlier, confirm the directory is listed in `scan_dirs` in `config/packages/mcp.php`. Since 6.7.15.0, capabilities are registered from their tag and `scan_dirs` no longer exists. + +## Authentication issues + +### HTTP 401 with JSON-RPC code `-32001` + +Since Shopware 6.7.15.0, `/api/_mcp` answers a missing, invalid, or expired bearer token with HTTP 401 and the JSON-RPC error code `-32001`. The message contains the OAuth reason, for example `Missing "Authorization" header` or `Access token is expired`. Clients that support token refresh use this status to request a new token. + +Before 6.7.15.0, the same cases returned HTTP 500 with the code `-32000`, which clients treated as a server outage. `/store-api/_mcp` already answered with 401. ## Security layers @@ -111,7 +124,7 @@ flowchart LR Which allowlist applies depends on the auth method: integration credentials use the integration allowlist; user access keys and user bearer tokens use the per-user allowlist; admin user accounts (`admin = true`) bypass the allowlist entirely. When an app forwards `sw-app-user-id` alongside integration credentials (e.g., Copilot), Shopware applies the **intersection** of the integration and user allowlists. -Bearer tokens obtained through the Administration login (`client_id = administration`) resolve to no integration, so they are treated as unrestricted. Test allowlist behavior with integration credentials or a user access key, never with a token copied out of the Administration. +Bearer tokens obtained through the Administration login (`client_id = administration`) resolve to the logged-in user. They use that user's allowlist, and an admin user bypasses it. Test allowlist behavior with integration credentials or with a non-admin user, never with a token copied out of the Administration session of an admin user. The Store API endpoint has no allowlist layer at all — see [Store API MCP](./store-api.md).