From d480dcbcc1a146f7a74ce6bf86502693a1352141 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Wed, 23 Sep 2026 14:50:00 +0200 Subject: [PATCH 1/5] docs: update MCP server docs for 6.7.15 and 6.7.16 Cover symfony/mcp-bundle 0.13, connect-time toolset selection, 401 on OAuth errors, the fail-closed MCP allowlist, and entity tool parameter contracts. --- .wordlist.txt | 2 + guides/plugins/plugins/mcp-server.md | 7 +- products/tools/mcp-server/best-practices.md | 13 ++ products/tools/mcp-server/configuration.md | 193 ++++++++++++++---- .../tools/mcp-server/copilot-tool-approval.md | 2 +- products/tools/mcp-server/getting-started.md | 38 +++- products/tools/mcp-server/index.md | 2 + products/tools/mcp-server/intro.md | 16 +- products/tools/mcp-server/store-api.md | 7 +- products/tools/mcp-server/tools-reference.md | 24 ++- products/tools/mcp-server/troubleshooting.md | 66 +++--- 11 files changed, 288 insertions(+), 82 deletions(-) diff --git a/.wordlist.txt b/.wordlist.txt index 8fa89afa71..0b22340e81 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -756,6 +756,7 @@ PHPUnit PHPunit PII PIM +PKCE POC POS PRs @@ -1334,6 +1335,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..87002aaa71 100644 --- a/guides/plugins/plugins/mcp-server.md +++ b/guides/plugins/plugins/mcp-server.md @@ -103,6 +103,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`, see [Best Practices](../../../products/tools/mcp-server/best-practices.md#write-tool-descriptions-for-agent-routing). - 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 +180,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 +210,10 @@ 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. + +Starting with Shopware 6.7.16.0, integrations and non-admin users can only call your tool after it is selected in their MCP allowlist. Tell operators which tools to select, or group them so that one toolset covers the use case. + ## Adding prompts Follow the same pattern with `#[McpPrompt]` and `shopware.mcp.prompt`: diff --git a/products/tools/mcp-server/best-practices.md b/products/tools/mcp-server/best-practices.md index ea6b134da8..89a877a5a0 100644 --- a/products/tools/mcp-server/best-practices.md +++ b/products/tools/mcp-server/best-practices.md @@ -101,6 +101,19 @@ The `description` field on `#[McpTool]` is the only signal the agent uses to pic **Make required parameters truly required.** Leave a parameter without a PHP default only if every prompt that should call this tool will include it. If the parameter is something the user rarely provides (e.g., a sales channel UUID or a tax ID), set it to `''` or `null` and validate it in the method. Required-but-missing parameters cause some agents to refuse the tool call entirely rather than ask the user. +**Describe every parameter with a `@param` docblock.** The MCP SDK renders the `@param` descriptions of `__invoke()` into `inputSchema.properties[*].description`, which is where a client reads the argument contract. Without them, a parameter reaches the client as a bare `{"type": "string"}`. This matters most for JSON-encoded string parameters: state whether an object or an array is expected, and give a short example. + +```php +/** + * @param string $aggregations A JSON ARRAY of aggregation definitions, as a string. + * Each element needs "name", "type" and "field", e.g. + * [{"name":"order_count","type":"count","field":"id"}]. + */ +public function __invoke(string $entity, string $aggregations): string +``` + +**Return the reason for rejecting invalid input.** An exception that escapes `__invoke()` reaches the client as a generic `Error while executing tool`, so the agent has nothing to correct on its retry. Catch the exceptions you expect and return a message that names the rejected part. Tools that extend `McpToolResponse` and parse Admin API criteria can pass a `SearchRequestException` to `$this->invalidCriteriaError()`, which lists every rejected element by its JSON pointer. Let unexpected exceptions propagate so that they are logged. + **Test descriptions with an LLM, not just a code reviewer.** A description that reads well to a developer can route badly. Run a small fixture set through the agent you target (Claude, GPT-4o) and compare expected versus selected tool. The cost of a routing failure is that the user does not get the tool they wanted; the cost of running the evaluation is a few hundred tokens. **Use automated checks as the regression net for descriptions and prompt wording.** Tool descriptions, system-prompt recipes, and resource lists are written in natural language. Changes to any of them can silently break routing on prompts that used to work. Maintain a small library of representative prompts ("how many products?", "ship order 10042", "upload this image as a product cover") with the expected tool selection, and run it before merging description changes the same way you run unit tests before merging behavior changes. diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index 604d265f39..76b1edb269 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -13,6 +13,10 @@ 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). + +Starting with 6.7.16.0, an unset MCP allowlist grants nothing instead of everything. See [Per-principal allowlist](#per-principal-allowlist). ::: ## Shopware MCP configuration @@ -33,7 +37,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 @@ -55,7 +59,8 @@ An empty list (the default) means no compile-time restriction; all registered to Removing the discovery tools at compile time prevents clients from finding and enabling the remaining tools. The per-integration and per-user allowlists in the Administration are the primary controls for day-to-day access management. -:::info Per-principal allowlist +### Per-principal allowlist + Shopware applies a per-principal MCP allowlist depending on how the client authenticates: | Auth mode | Allowlist source | @@ -63,14 +68,46 @@ 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 | -`null` per key means all capabilities of that type are allowed; a JSON array restricts access to the listed names; an empty array `[]` denies access to that capability type. +The allowlist is stored per capability type (`tools`, `resources`, `prompts`). A JSON array restricts access to the listed names, and an empty array `[]` denies access to that capability type. What an unset value means depends on the Shopware version: -The three server-owned discovery tools are the exception for tool allowlists. They remain available so that clients can use the discovery flow, but their search results and toolsets contain only tools permitted by the effective allowlist. +| Stored value | 6.7.16.0 and later | Before 6.7.16.0 | +| ----------------------------------------------------- | ----------------------------- | -------------------------------- | +| Column unset (`NULL`) | Nothing allowed | Everything allowed | +| Type key missing or `null`, such as `"prompts": null` | Nothing allowed for that type | Everything allowed for that type | +| Array of names | Only those names | Only those names | +| `[]` | Nothing allowed | Nothing allowed | + +Starting with 6.7.16.0, there is no stored value that means "everything" for an integration or a non-admin user. A principal that Shopware cannot resolve, such as an unknown access key or an inactive user, is blocked as well. In the Administration, the **All** switch of an integration or non-admin user saves the list of capabilities that exist at that moment. Capabilities that a plugin or app adds later must be selected explicitly. + +The three server-owned discovery tools are the exception for tool allowlists. They remain available so that clients can use the discovery flow, but their search results and toolsets contain only tools permitted by the effective allowlist. A principal without any selection therefore sees the three discovery tools, and they return nothing. Admin user accounts (`admin = true`) always bypass the allowlist regardless of auth mode. This applies to user accounts, not to integrations created with `--admin` (which bypasses ACL but still respects the per-integration allowlist). + +:::warning Upgrading to 6.7.16.0 +Existing integrations and non-admin users without an allowlist lose MCP access after the update. They still authenticate, but `tools/list` returns only the discovery tools, and calling a domain tool fails with `Tool "" is not enabled in your MCP allowlist.` Before updating, write down which capabilities each integration uses. Afterwards, select them under **Settings → Integrations → Edit MCP Allowlist** or on the user detail page, or send them to the API: + +```text +POST /api/_action/integration/{integrationId}/mcp-allowlist +POST /api/_action/user/{userId}/mcp-allowlist +``` + +```json +{ + "allowlist": { + "tools": ["shopware-entity-search", "shopware-entity-schema"], + "resources": ["shopware://entities"], + "prompts": [] + } +} +``` + +An allowlist stored as `{"tools": [...], "resources": null, "prompts": null}` keeps its tools and loses all resources and prompts. If you used the per-type **All** switch before, save the allowlist again. + +Both routes now also require the matching entity privilege, `user:update` or `integration:update`, and answer `403` without it. The `users_and_permissions.editor` role already grants `user:update`. A custom role that only has the action privilege must be extended. ::: ### Delegated user calls (`sw-app-user-id`) @@ -92,59 +129,92 @@ If the header is absent or invalid (i.e., not a valid UUID), Shopware ignores it When this header is present, and a valid user UUID is provided, Shopware applies the **intersection** of the integration allowlist and the user allowlist. A tool is only available if both the integration and the user have it enabled: -| Integration allowlist | User allowlist | Effective allowlist | -| --------------------- | --------------------- | ------------------- | -| `null` (unrestricted) | `null` (unrestricted) | unrestricted | -| `null` | `[tool-b]` | `[tool-b]` | -| `[tool-a, tool-b]` | `null` | `[tool-a, tool-b]` | -| `[tool-a, tool-b]` | `[tool-b, tool-c]` | `[tool-b]` | -| `[tool-a]` | `[]` | `[]` (nothing) | +| Integration allowlist | User allowlist | Effective allowlist | +| --------------------- | ------------------ | ---------------------------------------------- | +| `[tool-a, tool-b]` | admin user | `[tool-a, tool-b]` | +| `[tool-a, tool-b]` | `[tool-b, tool-c]` | `[tool-b]` | +| `[tool-a]` | `[]` | `[]` (nothing) | +| `[tool-a, tool-b]` | unset | `[]` since 6.7.16.0, `[tool-a, tool-b]` before | +| unset | `[tool-b]` | `[]` since 6.7.16.0, `[tool-b]` before | -Admin users bypass the user side of the intersection — if the user is an admin, their allowlist is treated as `null` (unrestricted), so the integration allowlist alone applies. +Admin users bypass the user side of the intersection, so the integration allowlist alone applies. Integrations never bypass the allowlist. Starting with 6.7.16.0, an app that forwards `sw-app-user-id` therefore needs an explicit integration allowlist, and every non-admin user needs their own selection. This pattern lets the app owner control which tools the integration may ever call, while users control which of those tools they personally allow the app to use on their behalf. Neither side can grant more than what the other has permitted. ## 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.*` | -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. +For both servers, the bundle's HTTP and stdio transports are switched off (`transports.http: false`), because Shopware's own controllers serve the endpoints. + +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. + +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/`. Store API sessions that existed before the update are not carried over, so Store API clients initialize once more. + +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. -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. +The `session` options of each server select the store: -| 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 | +| `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 +223,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 +239,8 @@ framework: If you already have a Redis/Valkey connection configured for Shopware, set `provider` to the same DSN to avoid opening a second connection. +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 +261,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 +269,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`: + +```bash +bin/console debug:mcp --scope=api # /api/_mcp only +bin/console debug:mcp --scope=store-api # /store-api/_mcp only +``` -Filter by capability type: +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 +298,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 +317,39 @@ 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 | Admin API | Store API | +| -------------------- | -------------------------------- | ------------------------------------ | +| `mcp.server` | `mcp.server.admin` | `mcp.server.store_api` | +| `mcp.server.builder` | `mcp.server.admin.builder` | `mcp.server.store_api.builder` | +| `mcp.registry` | `mcp.server.admin.registry` | `mcp.server.store_api.registry` | +| `mcp.session.store` | `mcp.server.admin.session.store` | `mcp.server.store_api.session.store` | + +The `mcp.store_api.registry`, `mcp.store_api.server.builder`, and `mcp.store_api.server` services and the `StoreApiMcpServerBuilderCompilerPass` were 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/copilot-tool-approval.md b/products/tools/mcp-server/copilot-tool-approval.md index 2d8dc5ee73..3b25e406a8 100644 --- a/products/tools/mcp-server/copilot-tool-approval.md +++ b/products/tools/mcp-server/copilot-tool-approval.md @@ -40,7 +40,7 @@ If your tool has side effects, declare `dryRun`. This is the whole contract. ## How Copilot uses your tools -Copilot connects to the shop's MCP endpoint with the merchant's permissions. Your tools aren't visible to the AI model by default. Shopware groups tools into toolsets using the group you declare, and Copilot enables a toolset only when the conversation needs it. +Copilot connects to the shop's MCP endpoint with the merchant's permissions. Starting with Shopware 6.7.16.0, your tool must also be selected in the MCP allowlist of the Copilot integration, and a non-admin merchant must select it in their own allowlist. See [Delegated user calls](./configuration.md#delegated-user-calls-sw-app-user-id). Your tools aren't visible to the AI model by default. Shopware groups tools into toolsets using the group you declare, and Copilot enables a toolset only when the conversation needs it. Two things matter for your tool to be picked at all: diff --git a/products/tools/mcp-server/getting-started.md b/products/tools/mcp-server/getting-started.md index c1e33a0ac4..4baded2bf5 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 @@ -31,6 +31,8 @@ SHOPWARE_SECRET_ACCESS_KEY=... :::info Restrict access The `--admin` flag grants full Admin API access. For production use, omit `--admin`, create a dedicated ACL role with only the required permissions, and assign it to the integration. See [Configuration](./configuration.md#acl-and-permissions) for details. + +The `--admin` flag does not bypass the MCP allowlist. Starting with Shopware 6.7.16.0, select the integration's capabilities before the client can use any tool, see [Controlling which capabilities are available](#controlling-which-capabilities-are-available). ::: ## Step 2: Configure your AI client @@ -124,7 +126,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 +149,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,15 +180,22 @@ 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: +Which capabilities an integration may call depends on its allowlist and on the Shopware version: + +- **6.7.16.0 and later:** An integration without an allowlist may call nothing. Only the three discovery tools are available, and they return nothing until you select capabilities. The same applies to non-admin users. Admin users bypass the allowlist. +- **Before 6.7.16.0:** An integration without an allowlist may call all registered tools, resources, and prompts. + +In both cases, only the discovery tools are advertised at the start of a session. To select which capabilities can be discovered and called: **Per integration** — Go to **Settings → Integrations**, open the context menu for your integration, and select **Edit MCP Allowlist**: Edit MCP Allowlist action in the Integrations list -Disable the toggle for each capability type and select only the tools, resources, and prompts this integration should use: +Select the tools, resources, and prompts this integration should use. On versions before 6.7.16.0, disable the **All** toggle of a capability type first: Capability selection modal diff --git a/products/tools/mcp-server/index.md b/products/tools/mcp-server/index.md index d9b83b6ce0..e4d21b3d1e 100644 --- a/products/tools/mcp-server/index.md +++ b/products/tools/mcp-server/index.md @@ -159,6 +159,8 @@ 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 +Explicit MCP allowlists for integrations and non-admin users in Shopware 6.7.16.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..5fee864d56 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. Shopware 6.7.16.0 changes an unset MCP allowlist from "everything allowed" to "nothing allowed". 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..323922e90a 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. @@ -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..87c0c9ba28 100644 --- a/products/tools/mcp-server/tools-reference.md +++ b/products/tools/mcp-server/tools-reference.md @@ -27,6 +27,16 @@ All core tools return a consistent JSON envelope via `McpToolResponse`: The `_meta` field is optional and used for pagination context, dry-run status, and sales channel scope. +The entity tools and `shopware-theme-config` describe each parameter in their `inputSchema`. Parameters that take a JSON-encoded string, such as `criteria` or `aggregations`, state whether an object or an array is expected and include an example. + +When `shopware-entity-search` or `shopware-entity-aggregate` receives criteria or aggregations it cannot parse, the error names every rejected element by its JSON pointer: + +```json +{"success": false, "error": "Invalid criteria: /aggregations/0/avg/field: The aggregation should contain a \"field\"."} +``` + +Correct the named element and call the tool again. + ## Discovery tools With core alone, a fresh MCP session advertises only the three discovery tools below; every other core tool becomes visible when its toolset is enabled for the session. A plugin or bundle can add its own tool to the `discovery` group, in which case that tool is advertised from the start as well — see [Assign a tool group](../../../guides/plugins/plugins/mcp-server.md#step-2-assign-a-tool-group). @@ -64,6 +74,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: @@ -133,7 +145,7 @@ Get the field and association schema of any Shopware entity. Use this first to d ### shopware-entity-search -Search entity records using Admin API criteria. Returns entity rows with pagination metadata. Does **not** return aggregation results; use `shopware-entity-aggregate` for counts, averages, and other metrics. +Search, list, and page through records of any entity type, such as orders, products, customers, or categories, using Admin API criteria. Returns entity rows with pagination metadata. Use `criteria.sort` for requests such as "the last 10 orders". Does **not** return aggregation results; use `shopware-entity-aggregate` for counts, averages, and other metrics. **Response optimization:** When no `includes` are specified in the criteria, responses are automatically trimmed to scalar fields and explicitly requested associations. This strips thumbnails, extensions, and translated duplicates, keeping responses well within the 100 KB limit. @@ -171,11 +183,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. Each element needs `name`, `type`, and `field`. 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..5cb0e0c48f 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,27 +9,34 @@ 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 | Refresh the token; the error 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 | +| All tool calls rejected after updating to 6.7.16.0 | Integration or non-admin user has no allowlist | Select capabilities, see [Per-principal allowlist](./configuration.md#per-principal-allowlist) | +| Resources or prompts gone after updating to 6.7.16.0 | Allowlist stores `null` for that type | Save the allowlist again with the capabilities selected | +| `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) | +| `Invalid criteria: /aggregations/0/...` | Malformed criteria or aggregation definition | Fix the element named by the JSON pointer and retry | +| 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 | +| Store API 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 +94,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 @@ -107,11 +123,11 @@ flowchart LR **Authentication (Layer 1):** Pass `sw-access-key` and `sw-secret-access-key` headers. Obtain credentials from Settings → Integrations. -**MCP Allowlist (Layer 2):** Allowlists are scoped per principal: each integration has its own allowlist under **Settings → Integrations → Edit MCP Allowlist**, and each user has their own under **Settings → Users & Permissions → [user] → MCP tool allowlist**. `null` per type means all capabilities of that type are accessible; an empty array `[]` means none are accessible. The three server-owned discovery tools remain available, but cannot expose or enable tools denied by the effective allowlist. The `admin` flag on an integration does **not** bypass the allowlist; it only bypasses layer 3 (ACL). +**MCP Allowlist (Layer 2):** Allowlists are scoped per principal: each integration has its own allowlist under **Settings → Integrations → Edit MCP Allowlist**, and each user has their own under **Settings → Users & Permissions → [user] → MCP tool allowlist**. An array lists the accessible capabilities of a type, and an empty array `[]` means none are accessible. Since Shopware 6.7.16.0, an unset allowlist or a `null` type also means none are accessible. Before 6.7.16.0, it meant all of them. The three server-owned discovery tools remain available, but cannot expose or enable tools denied by the effective allowlist. The `admin` flag on an integration does **not** bypass the allowlist; it only bypasses layer 3 (ACL). 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). From 42d8c221444aeefc796b6b6b867b0afb320c4122 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Thu, 24 Sep 2026 13:42:41 +0200 Subject: [PATCH 2/5] docs(mcp): verify the Redis session store and share the session registry Checked against a trunk lane on mcp-bundle 0.13 / mcp/sdk 0.8.1: the per-server `cache` recipe puts both stores on Psr16SessionStore with separate prefixes, sessions survive losing the local cache dir, and a removed Redis key is refused with "Session not found or has expired." A recipe that is not picked up leaves the file store running silently, so add the debug:container check that tells the two apart. The active session registry that app-driven tools/list_changed broadcasts fan out to lives in cache.system, which is per server. Document pointing both registries at the shared pool, and the lock store they depend on. --- products/tools/mcp-server/configuration.md | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index 76b1edb269..f56e33f681 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -239,6 +239,34 @@ 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. 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 From 25e761c0ca40454cc764117ef1bd30e71df59428 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Thu, 24 Sep 2026 14:50:45 +0200 Subject: [PATCH 3/5] docs(mcp): state the lifetime the shared session registry needs The registry is written without a TTL and falls back to the pool's default_lifetime. It is rewritten on every MCP request, so it outlives its sessions unless default_lifetime is shorter than session.ttl. --- products/tools/mcp-server/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index f56e33f681..8a0e993254 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -265,7 +265,7 @@ services: arguments: ['@cache.mcp_sessions'] ``` -The two registries use different cache keys, so they can share the pool with the session stores. 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. +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. From 44bba4489cb0393299c1ed257b97c4344360aad3 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Wed, 7 Oct 2026 09:23:22 +0200 Subject: [PATCH 4/5] docs(mcp): limit the MCP server update to 6.7.15.0 The 6.7.16.0 changes (unset allowlist grants nothing, entity tool parameter descriptions and criteria errors) move to their own PR, because 6.7.16.0 is not released yet. --- guides/plugins/plugins/mcp-server.md | 4 +- products/tools/mcp-server/best-practices.md | 13 ----- products/tools/mcp-server/configuration.md | 58 ++++--------------- .../tools/mcp-server/copilot-tool-approval.md | 2 +- products/tools/mcp-server/getting-started.md | 13 +---- products/tools/mcp-server/index.md | 1 - products/tools/mcp-server/intro.md | 2 +- products/tools/mcp-server/tools-reference.md | 22 ++----- products/tools/mcp-server/troubleshooting.md | 55 +++++++++--------- 9 files changed, 50 insertions(+), 120 deletions(-) diff --git a/guides/plugins/plugins/mcp-server.md b/guides/plugins/plugins/mcp-server.md index 87002aaa71..3516923de2 100644 --- a/guides/plugins/plugins/mcp-server.md +++ b/guides/plugins/plugins/mcp-server.md @@ -103,7 +103,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`, see [Best Practices](../../../products/tools/mcp-server/best-practices.md#write-tool-descriptions-for-agent-routing). +- 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. @@ -212,8 +212,6 @@ If the tool appears here, it is registered in the live HTTP endpoint and can be Since Shopware 6.7.15.0, `debug:mcp` lists tools only. Use `bin/console debug:mcp --native` to verify prompts and resources. -Starting with Shopware 6.7.16.0, integrations and non-admin users can only call your tool after it is selected in their MCP allowlist. Tell operators which tools to select, or group them so that one toolset covers the use case. - ## Adding prompts Follow the same pattern with `#[McpPrompt]` and `shopware.mcp.prompt`: diff --git a/products/tools/mcp-server/best-practices.md b/products/tools/mcp-server/best-practices.md index 89a877a5a0..ea6b134da8 100644 --- a/products/tools/mcp-server/best-practices.md +++ b/products/tools/mcp-server/best-practices.md @@ -101,19 +101,6 @@ The `description` field on `#[McpTool]` is the only signal the agent uses to pic **Make required parameters truly required.** Leave a parameter without a PHP default only if every prompt that should call this tool will include it. If the parameter is something the user rarely provides (e.g., a sales channel UUID or a tax ID), set it to `''` or `null` and validate it in the method. Required-but-missing parameters cause some agents to refuse the tool call entirely rather than ask the user. -**Describe every parameter with a `@param` docblock.** The MCP SDK renders the `@param` descriptions of `__invoke()` into `inputSchema.properties[*].description`, which is where a client reads the argument contract. Without them, a parameter reaches the client as a bare `{"type": "string"}`. This matters most for JSON-encoded string parameters: state whether an object or an array is expected, and give a short example. - -```php -/** - * @param string $aggregations A JSON ARRAY of aggregation definitions, as a string. - * Each element needs "name", "type" and "field", e.g. - * [{"name":"order_count","type":"count","field":"id"}]. - */ -public function __invoke(string $entity, string $aggregations): string -``` - -**Return the reason for rejecting invalid input.** An exception that escapes `__invoke()` reaches the client as a generic `Error while executing tool`, so the agent has nothing to correct on its retry. Catch the exceptions you expect and return a message that names the rejected part. Tools that extend `McpToolResponse` and parse Admin API criteria can pass a `SearchRequestException` to `$this->invalidCriteriaError()`, which lists every rejected element by its JSON pointer. Let unexpected exceptions propagate so that they are logged. - **Test descriptions with an LLM, not just a code reviewer.** A description that reads well to a developer can route badly. Run a small fixture set through the agent you target (Claude, GPT-4o) and compare expected versus selected tool. The cost of a routing failure is that the user does not get the tool they wanted; the cost of running the evaluation is a few hundred tokens. **Use automated checks as the regression net for descriptions and prompt wording.** Tool descriptions, system-prompt recipes, and resource lists are written in natural language. Changes to any of them can silently break routing on prompts that used to work. Maintain a small library of representative prompts ("how many products?", "ship order 10042", "upload this image as a product cover") with the expected tool selection, and run it before merging description changes the same way you run unit tests before merging behavior changes. diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index 8a0e993254..4e691b740b 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -15,8 +15,6 @@ On Shopware 6.7.11.0 to 6.7.13.x, the MCP server is gated behind the `MCP_SERVER 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). - -Starting with 6.7.16.0, an unset MCP allowlist grants nothing instead of everything. See [Per-principal allowlist](#per-principal-allowlist). ::: ## Shopware MCP configuration @@ -59,8 +57,7 @@ An empty list (the default) means no compile-time restriction; all registered to Removing the discovery tools at compile time prevents clients from finding and enabling the remaining tools. The per-integration and per-user allowlists in the Administration are the primary controls for day-to-day access management. -### Per-principal allowlist - +:::info Per-principal allowlist Shopware applies a per-principal MCP allowlist depending on how the client authenticates: | Auth mode | Allowlist source | @@ -72,42 +69,11 @@ Shopware applies a per-principal MCP allowlist depending on how the client authe | Bearer JWT, client_credentials | Per-integration allowlist | | Integration + `sw-app-user-id` (Copilot) | Intersection of the integration allowlist and the user allowlist | -The allowlist is stored per capability type (`tools`, `resources`, `prompts`). A JSON array restricts access to the listed names, and an empty array `[]` denies access to that capability type. What an unset value means depends on the Shopware version: - -| Stored value | 6.7.16.0 and later | Before 6.7.16.0 | -| ----------------------------------------------------- | ----------------------------- | -------------------------------- | -| Column unset (`NULL`) | Nothing allowed | Everything allowed | -| Type key missing or `null`, such as `"prompts": null` | Nothing allowed for that type | Everything allowed for that type | -| Array of names | Only those names | Only those names | -| `[]` | Nothing allowed | Nothing allowed | +`null` per key means all capabilities of that type are allowed; a JSON array restricts access to the listed names; an empty array `[]` denies access to that capability type. -Starting with 6.7.16.0, there is no stored value that means "everything" for an integration or a non-admin user. A principal that Shopware cannot resolve, such as an unknown access key or an inactive user, is blocked as well. In the Administration, the **All** switch of an integration or non-admin user saves the list of capabilities that exist at that moment. Capabilities that a plugin or app adds later must be selected explicitly. - -The three server-owned discovery tools are the exception for tool allowlists. They remain available so that clients can use the discovery flow, but their search results and toolsets contain only tools permitted by the effective allowlist. A principal without any selection therefore sees the three discovery tools, and they return nothing. +The three server-owned discovery tools are the exception for tool allowlists. They remain available so that clients can use the discovery flow, but their search results and toolsets contain only tools permitted by the effective allowlist. Admin user accounts (`admin = true`) always bypass the allowlist regardless of auth mode. This applies to user accounts, not to integrations created with `--admin` (which bypasses ACL but still respects the per-integration allowlist). - -:::warning Upgrading to 6.7.16.0 -Existing integrations and non-admin users without an allowlist lose MCP access after the update. They still authenticate, but `tools/list` returns only the discovery tools, and calling a domain tool fails with `Tool "" is not enabled in your MCP allowlist.` Before updating, write down which capabilities each integration uses. Afterwards, select them under **Settings → Integrations → Edit MCP Allowlist** or on the user detail page, or send them to the API: - -```text -POST /api/_action/integration/{integrationId}/mcp-allowlist -POST /api/_action/user/{userId}/mcp-allowlist -``` - -```json -{ - "allowlist": { - "tools": ["shopware-entity-search", "shopware-entity-schema"], - "resources": ["shopware://entities"], - "prompts": [] - } -} -``` - -An allowlist stored as `{"tools": [...], "resources": null, "prompts": null}` keeps its tools and loses all resources and prompts. If you used the per-type **All** switch before, save the allowlist again. - -Both routes now also require the matching entity privilege, `user:update` or `integration:update`, and answer `403` without it. The `users_and_permissions.editor` role already grants `user:update`. A custom role that only has the action privilege must be extended. ::: ### Delegated user calls (`sw-app-user-id`) @@ -129,15 +95,15 @@ If the header is absent or invalid (i.e., not a valid UUID), Shopware ignores it When this header is present, and a valid user UUID is provided, Shopware applies the **intersection** of the integration allowlist and the user allowlist. A tool is only available if both the integration and the user have it enabled: -| Integration allowlist | User allowlist | Effective allowlist | -| --------------------- | ------------------ | ---------------------------------------------- | -| `[tool-a, tool-b]` | admin user | `[tool-a, tool-b]` | -| `[tool-a, tool-b]` | `[tool-b, tool-c]` | `[tool-b]` | -| `[tool-a]` | `[]` | `[]` (nothing) | -| `[tool-a, tool-b]` | unset | `[]` since 6.7.16.0, `[tool-a, tool-b]` before | -| unset | `[tool-b]` | `[]` since 6.7.16.0, `[tool-b]` before | +| Integration allowlist | User allowlist | Effective allowlist | +| --------------------- | --------------------- | ------------------- | +| `null` (unrestricted) | `null` (unrestricted) | unrestricted | +| `null` | `[tool-b]` | `[tool-b]` | +| `[tool-a, tool-b]` | `null` | `[tool-a, tool-b]` | +| `[tool-a, tool-b]` | `[tool-b, tool-c]` | `[tool-b]` | +| `[tool-a]` | `[]` | `[]` (nothing) | -Admin users bypass the user side of the intersection, so the integration allowlist alone applies. Integrations never bypass the allowlist. Starting with 6.7.16.0, an app that forwards `sw-app-user-id` therefore needs an explicit integration allowlist, and every non-admin user needs their own selection. +Admin users bypass the user side of the intersection — if the user is an admin, their allowlist is treated as `null` (unrestricted), so the integration allowlist alone applies. This pattern lets the app owner control which tools the integration may ever call, while users control which of those tools they personally allow the app to use on their behalf. Neither side can grant more than what the other has permitted. @@ -184,7 +150,7 @@ Each MCP server has its own session store. Session IDs are not namespaced per se 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/`. Store API sessions that existed before the update are not carried over, so Store API clients initialize once more. -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. +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: diff --git a/products/tools/mcp-server/copilot-tool-approval.md b/products/tools/mcp-server/copilot-tool-approval.md index 3b25e406a8..2d8dc5ee73 100644 --- a/products/tools/mcp-server/copilot-tool-approval.md +++ b/products/tools/mcp-server/copilot-tool-approval.md @@ -40,7 +40,7 @@ If your tool has side effects, declare `dryRun`. This is the whole contract. ## How Copilot uses your tools -Copilot connects to the shop's MCP endpoint with the merchant's permissions. Starting with Shopware 6.7.16.0, your tool must also be selected in the MCP allowlist of the Copilot integration, and a non-admin merchant must select it in their own allowlist. See [Delegated user calls](./configuration.md#delegated-user-calls-sw-app-user-id). Your tools aren't visible to the AI model by default. Shopware groups tools into toolsets using the group you declare, and Copilot enables a toolset only when the conversation needs it. +Copilot connects to the shop's MCP endpoint with the merchant's permissions. Your tools aren't visible to the AI model by default. Shopware groups tools into toolsets using the group you declare, and Copilot enables a toolset only when the conversation needs it. Two things matter for your tool to be picked at all: diff --git a/products/tools/mcp-server/getting-started.md b/products/tools/mcp-server/getting-started.md index 4baded2bf5..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). Selecting toolsets in the connection URL requires 6.7.15.0 or later. +- 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 @@ -31,8 +31,6 @@ SHOPWARE_SECRET_ACCESS_KEY=... :::info Restrict access The `--admin` flag grants full Admin API access. For production use, omit `--admin`, create a dedicated ACL role with only the required permissions, and assign it to the integration. See [Configuration](./configuration.md#acl-and-permissions) for details. - -The `--admin` flag does not bypass the MCP allowlist. Starting with Shopware 6.7.16.0, select the integration's capabilities before the client can use any tool, see [Controlling which capabilities are available](#controlling-which-capabilities-are-available). ::: ## Step 2: Configure your AI client @@ -184,18 +182,13 @@ A missing, invalid, or expired token is answered with HTTP 401 and the JSON-RPC ## Controlling which capabilities are available -Which capabilities an integration may call depends on its allowlist and on the Shopware version: - -- **6.7.16.0 and later:** An integration without an allowlist may call nothing. Only the three discovery tools are available, and they return nothing until you select capabilities. The same applies to non-admin users. Admin users bypass the allowlist. -- **Before 6.7.16.0:** An integration without an allowlist may call all registered tools, resources, and prompts. - -In both cases, only the discovery tools are advertised at the start of a session. To select which capabilities can be discovered and called: +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: **Per integration** — Go to **Settings → Integrations**, open the context menu for your integration, and select **Edit MCP Allowlist**: Edit MCP Allowlist action in the Integrations list -Select the tools, resources, and prompts this integration should use. On versions before 6.7.16.0, disable the **All** toggle of a capability type first: +Disable the toggle for each capability type and select only the tools, resources, and prompts this integration should use: Capability selection modal diff --git a/products/tools/mcp-server/index.md b/products/tools/mcp-server/index.md index e4d21b3d1e..8a7f1a4f08 100644 --- a/products/tools/mcp-server/index.md +++ b/products/tools/mcp-server/index.md @@ -160,7 +160,6 @@ The Shopware MCP Server is currently available as an experimental feature. 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 -Explicit MCP allowlists for integrations and non-admin users in Shopware 6.7.16.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 5fee864d56..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. Shopware 6.7.15.0 moves to `symfony/mcp-bundle` 0.13 and adds connect-time toolset selection. Shopware 6.7.16.0 changes an unset MCP allowlist from "everything allowed" to "nothing allowed". 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 diff --git a/products/tools/mcp-server/tools-reference.md b/products/tools/mcp-server/tools-reference.md index 87c0c9ba28..fd7085cdf6 100644 --- a/products/tools/mcp-server/tools-reference.md +++ b/products/tools/mcp-server/tools-reference.md @@ -27,16 +27,6 @@ All core tools return a consistent JSON envelope via `McpToolResponse`: The `_meta` field is optional and used for pagination context, dry-run status, and sales channel scope. -The entity tools and `shopware-theme-config` describe each parameter in their `inputSchema`. Parameters that take a JSON-encoded string, such as `criteria` or `aggregations`, state whether an object or an array is expected and include an example. - -When `shopware-entity-search` or `shopware-entity-aggregate` receives criteria or aggregations it cannot parse, the error names every rejected element by its JSON pointer: - -```json -{"success": false, "error": "Invalid criteria: /aggregations/0/avg/field: The aggregation should contain a \"field\"."} -``` - -Correct the named element and call the tool again. - ## Discovery tools With core alone, a fresh MCP session advertises only the three discovery tools below; every other core tool becomes visible when its toolset is enabled for the session. A plugin or bundle can add its own tool to the `discovery` group, in which case that tool is advertised from the start as well — see [Assign a tool group](../../../guides/plugins/plugins/mcp-server.md#step-2-assign-a-tool-group). @@ -145,7 +135,7 @@ Get the field and association schema of any Shopware entity. Use this first to d ### shopware-entity-search -Search, list, and page through records of any entity type, such as orders, products, customers, or categories, using Admin API criteria. Returns entity rows with pagination metadata. Use `criteria.sort` for requests such as "the last 10 orders". Does **not** return aggregation results; use `shopware-entity-aggregate` for counts, averages, and other metrics. +Search entity records using Admin API criteria. Returns entity rows with pagination metadata. Does **not** return aggregation results; use `shopware-entity-aggregate` for counts, averages, and other metrics. **Response optimization:** When no `includes` are specified in the criteria, responses are automatically trimmed to scalar fields and explicitly requested associations. This strips thumbnails, extensions, and translated duplicates, keeping responses well within the 100 KB limit. @@ -183,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. Each element needs `name`, `type`, and `field`. A single object instead of an array is rejected | -| `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 5cb0e0c48f..c8a3b531ed 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,34 +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 | -| HTTP 401 with JSON-RPC code `-32001` | Bearer token missing, invalid, or expired | Refresh the token; the error 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 | -| All tool calls rejected after updating to 6.7.16.0 | Integration or non-admin user has no allowlist | Select capabilities, see [Per-principal allowlist](./configuration.md#per-principal-allowlist) | -| Resources or prompts gone after updating to 6.7.16.0 | Allowlist stores `null` for that type | Save the allowlist again with the capabilities selected | -| `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) | -| `Invalid criteria: /aggregations/0/...` | Malformed criteria or aggregation definition | Fix the element named by the JSON pointer and retry | -| 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 | -| Store API 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 | +| 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 | Refresh the token; the error 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 | +| Store API 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 @@ -123,7 +120,7 @@ flowchart LR **Authentication (Layer 1):** Pass `sw-access-key` and `sw-secret-access-key` headers. Obtain credentials from Settings → Integrations. -**MCP Allowlist (Layer 2):** Allowlists are scoped per principal: each integration has its own allowlist under **Settings → Integrations → Edit MCP Allowlist**, and each user has their own under **Settings → Users & Permissions → [user] → MCP tool allowlist**. An array lists the accessible capabilities of a type, and an empty array `[]` means none are accessible. Since Shopware 6.7.16.0, an unset allowlist or a `null` type also means none are accessible. Before 6.7.16.0, it meant all of them. The three server-owned discovery tools remain available, but cannot expose or enable tools denied by the effective allowlist. The `admin` flag on an integration does **not** bypass the allowlist; it only bypasses layer 3 (ACL). +**MCP Allowlist (Layer 2):** Allowlists are scoped per principal: each integration has its own allowlist under **Settings → Integrations → Edit MCP Allowlist**, and each user has their own under **Settings → Users & Permissions → [user] → MCP tool allowlist**. `null` per type means all capabilities of that type are accessible; an empty array `[]` means none are accessible. The three server-owned discovery tools remain available, but cannot expose or enable tools denied by the effective allowlist. The `admin` flag on an integration does **not** bypass the allowlist; it only bypasses layer 3 (ACL). 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. From 958e89051c056fbd3fcb3b71918cfe4efe186458 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Wed, 7 Oct 2026 12:01:21 +0200 Subject: [PATCH 5/5] docs(mcp): apply review feedback on the 6.7.15.0 MCP docs Both endpoints lose their sessions on the update, the Store API services have direct replacements, the Store API session text no longer claims a shared store, the tool example documents its parameters, and a missing bearer token is told apart from an expired one. --- guides/plugins/plugins/mcp-server.md | 4 ++ products/tools/mcp-server/configuration.md | 21 ++++---- products/tools/mcp-server/store-api.md | 2 +- products/tools/mcp-server/troubleshooting.md | 50 ++++++++++---------- 4 files changed, 42 insertions(+), 35 deletions(-) diff --git a/guides/plugins/plugins/mcp-server.md b/guides/plugins/plugins/mcp-server.md index 3516923de2..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(); diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index 4e691b740b..1407863f15 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -148,7 +148,7 @@ MCP sessions track an ongoing conversation across multiple requests. The client 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. -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/`. Store API sessions that existed before the update are not carried over, so Store API clients initialize once more. +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. 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. @@ -320,14 +320,17 @@ Code that integrates with the MCP internals must be updated. These classes and s The bundle registers one set of services per server, so the flat service IDs were renamed: -| Before | Admin API | Store API | -| -------------------- | -------------------------------- | ------------------------------------ | -| `mcp.server` | `mcp.server.admin` | `mcp.server.store_api` | -| `mcp.server.builder` | `mcp.server.admin.builder` | `mcp.server.store_api.builder` | -| `mcp.registry` | `mcp.server.admin.registry` | `mcp.server.store_api.registry` | -| `mcp.session.store` | `mcp.server.admin.session.store` | `mcp.server.store_api.session.store` | - -The `mcp.store_api.registry`, `mcp.store_api.server.builder`, and `mcp.store_api.server` services and the `StoreApiMcpServerBuilderCompilerPass` were removed. +| 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: diff --git a/products/tools/mcp-server/store-api.md b/products/tools/mcp-server/store-api.md index 323922e90a..1e14cce142 100644 --- a/products/tools/mcp-server/store-api.md +++ b/products/tools/mcp-server/store-api.md @@ -95,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). diff --git a/products/tools/mcp-server/troubleshooting.md b/products/tools/mcp-server/troubleshooting.md index c8a3b531ed..a0c9c1b56d 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,31 +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 | -| HTTP 401 with JSON-RPC code `-32001` | Bearer token missing, invalid, or expired | Refresh the token; the error 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 | -| Store API 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 | +| 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