diff --git a/guides/hosting/infrastructure/rate-limiter.md b/guides/hosting/infrastructure/rate-limiter.md index 3b52ff8dec..b65c3988a7 100644 --- a/guides/hosting/infrastructure/rate-limiter.md +++ b/guides/hosting/infrastructure/rate-limiter.md @@ -21,24 +21,26 @@ These rate limiters are always active. You do not need to add the `shopware.api. The following limiters are enabled by default. The values listed here reflect the current Shopware core configuration and may change between versions. When in doubt, the source of truth is `src/Core/Framework/Resources/config/packages/shopware.yaml` in the version you are running. -| Limiter | Protects | Since | Policy | Reset | Limits | -| ----------------------------- | ------------------------------------------------------------------- | -------- | ---------------- | -------- | ---------------------------------------------------------- | -| `login` | Storefront / Store-API customer authentication | 6.4.6.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | -| `guest_login` | Storefront / Store-API after-order guest authentication | 6.4.6.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | -| `oauth` | API OAuth authentication / Administration login | 6.4.6.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | -| `reset_password` | Storefront / Store-API customer password reset | 6.4.6.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | -| `user_recovery` | Administration user password recovery | 6.4.6.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | -| `contact_form` | Storefront / Store-API contact form | 6.4.6.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | -| `notification` | Store-API notification endpoint | 6.4.8.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | -| `newsletter_form` | Store-API newsletter registration | 6.4.16.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | -| `cart_add_line_item` | Adding line items to the cart | 6.4.18.0 | `system_config` | 1 hour | Limit read from `core.cart.lineItemAddLimit`, interval 60s | -| `revocation_request_form` | Store-API revocation request form | 6.7.9.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | -| `newsletter_unsubscribe_form` | Store-API newsletter unsubscribe | 6.7.9.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | -| `app_shop_verify` | App shop registration verification | 6.7.9.0 | `sliding_window` | n/a | 60 / 60min | -| `mcp_admin_api` | MCP server Admin API access (keyed per OAuth token) | 6.8.0.0 | `time_backoff` | 1 hour | 300 / 60s, 1000 / 10min | -| `mcp_store_api` | MCP server Store API access (keyed per sales-channel context token) | 6.8.0.0 | `time_backoff` | 1 hour | 120 / 60s, 600 / 10min | - -In the table above, `Limits` uses the `limit / interval` notation, and `s` / `min` are seconds / minutes. When you add a new default limiter to the core, add a row here with its introduction version so this table stays complete. +| Limiter | Protects | Since | Policy | Reset | Limits | +| ----------------------------- | ------------------------------------------------------------------------------------- | -------- | ---------------- | -------- | ---------------------------------------------------------- | +| `login` | Storefront / Store-API customer authentication | 6.4.6.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | +| `guest_login` | Storefront / Store-API after-order guest authentication | 6.4.6.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | +| `oauth` | API OAuth authentication / Administration login | 6.4.6.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | +| `reset_password` | Storefront / Store-API customer password reset | 6.4.6.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | +| `user_recovery` | Administration user password recovery | 6.4.6.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | +| `contact_form` | Storefront / Store-API contact form | 6.4.6.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | +| `notification` | Store-API notification endpoint | 6.4.8.0 | `time_backoff` | 24 hours | 10 / 10s, 15 / 30s, 20 / 60s | +| `newsletter_form` | Store-API newsletter registration | 6.4.16.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | +| `cart_add_line_item` | Adding line items to the cart | 6.4.18.0 | `system_config` | 1 hour | Limit read from `core.cart.lineItemAddLimit`, interval 60s | +| `revocation_request_form` | Store-API revocation request form | 6.7.9.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | +| `newsletter_unsubscribe_form` | Store-API newsletter unsubscribe | 6.7.9.0 | `time_backoff` | 24 hours | 3 / 30s, 5 / 60s, 10 / 90s | +| `app_shop_verify` | App shop registration verification | 6.7.9.0 | `sliding_window` | n/a | 60 / 60min | +| `mcp_admin_api` | MCP server Admin API access (keyed per OAuth token) | 6.7.12.0 | `sliding_window` | n/a | 300 / 60s | +| `mcp_store_api` | MCP server Store API access (keyed per sales-channel context token and per client IP) | 6.7.12.0 | `sliding_window` | n/a | 120 / 60s | + +In the table above, `Limits` uses the `limit / interval` notation, and `s` / `min` are seconds / minutes. + +Before Shopware 6.7.16.0, `mcp_admin_api` and `mcp_store_api` used `time_backoff` with a one-hour reset and additional limits of 1000 and 600 requests per 10 minutes. When you add a new default limiter to the core, add a row here with its introduction version so this table stays complete. ## Configuration diff --git a/guides/plugins/apps/mcp-server.md b/guides/plugins/apps/mcp-server.md index 073a795401..41a2fd3dec 100644 --- a/guides/plugins/apps/mcp-server.md +++ b/guides/plugins/apps/mcp-server.md @@ -56,10 +56,10 @@ Names must only contain `a-zA-Z0-9_-` (no dots). The `shopware-` prefix is reser Shopware automatically groups all tools from an active app into a toolset named after the app's technical name. No additional field in `mcp.xml` is required. An MCP client can find the tools with `shopware-tool-search` or enable the complete app toolset with `shopware-toolset-enable`. The toolset title is generated from the app name by replacing hyphens with spaces and capitalizing the first letter, so an app named `my-integration` produces the title "My integration tools". -When an app is installed, updated, activated, deactivated, or deleted, Shopware queues the relevant `notifications/*/list_changed` message for the active MCP sessions, and only for the capability types the app actually declares. Clients can then refresh their tools, prompts, or resources. +When an app is installed, updated, activated, deactivated, or deleted, Shopware queues the relevant `notifications/*/list_changed` message for the MCP sessions on the Admin API endpoint (`/api/_mcp`), and only for the capability types the app actually declares. Since Shopware 6.7.16.0, this reaches every session, whichever server it is connected to. Before, only sessions on the server that processed the app change were notified. Clients can then refresh their tools, prompts, or resources. :::info How notifications reach the client -Notifications are queued into the MCP session rather than pushed. A client receives them with the response to its next request, or immediately over an open streaming connection. A client that neither keeps a stream open nor sends another request sees nothing. Queuing also requires a configured session store — see [Session store](../../../products/tools/mcp-server/configuration.md#session-store). +Notifications are queued into the MCP session rather than pushed. Since Shopware 6.7.16.0, a session detects an app change on its next request, and the client receives the notification with a later response. A client that sends no further request sees nothing. Queuing also requires a configured session store — see [Session store](../../../products/tools/mcp-server/configuration.md#session-store). Only the app lifecycle emits these notifications. Plugins and Symfony bundles register their capabilities at container build time, so their changes are not announced to running sessions. ::: @@ -122,6 +122,8 @@ Each `` maps to a JSON Schema property in the tool's `inputSchema`: List the ACL privileges your tool needs with ``. The admin UI uses this list to warn operators when an integration's role is missing privileges a tool expects. +Every privilege in `` must also be declared in the `` of your manifest, because the tool runs with the app's integration role. Otherwise, the app fails to install or update, and the error names the tool and the missing privilege. Starting with Shopware 6.7.16.0, this also applies to apps without a `` block. Before, those apps skipped the check. + How strictly these privileges are enforced depends on the URL mode: - **External URL** (`https://...`): informational only. Shopware does not check these privileges before calling your webhook. Your app must enforce them on its own side - for example, by validating `source.shopId` against what that shop is permitted to do in your backend. @@ -271,7 +273,7 @@ app.post('/mcp/sync-orders', (req, res) => { ## Returning a response -Return JSON as the tool result body. Shopware forwards it to the AI client as-is: +Return JSON as the tool result body: ```json {"success": true, "data": {"synced": 42, "errors": 0}} @@ -279,6 +281,10 @@ Return JSON as the tool result body. Shopware forwards it to the AI client as-is For consistency with core tools, follow the `{"success": bool, "data": ..., "_meta": ...}` / `{"success": false, "error": "..."}` convention. Shopware does not reject responses that omit `success`, but it logs a warning when the field is missing so operators can spot broken handlers. +Starting with Shopware 6.7.16.0, Shopware keeps a body with a boolean `success` as the text block of the result and adds `structuredContent` with your `data`, `isError`, and `_meta["shopware/generatedAt"]`. A top-level `code`, or `error` as an object with `message` and `code`, becomes `structuredContent.error.code`. Other top-level keys become metadata. Any other body is passed on as text only. See [Response format](../../../products/tools/mcp-server/tools-reference.md#response-format). + +App tool results are never offloaded to the tool-result cache. Keep them well under 100 KB. + **Timeout:** App webhook calls time out after `shopware.mcp.app_tool_timeout` seconds (default: 10). Keep responses fast or use async patterns with status polling. ## Locale resolution @@ -299,6 +305,8 @@ bin/console debug:mcp Your tools appear with **Source: app** in the output. To see what a specific integration can reach (respecting its per-integration allowlist), pass `--integration=SWIA...` with the integration's access key. +Starting with Shopware 6.7.16.0, integrations and non-admin users can only call your tools after they are selected in their MCP allowlist. Tell operators which tools to select, see [Per-principal allowlist](../../../products/tools/mcp-server/configuration.md#per-principal-allowlist). + ## Further reading - [MCP Concepts](../../../products/tools/mcp-server/mcp-concepts.md): tools, resources, and prompts explained diff --git a/guides/plugins/plugins/mcp-server.md b/guides/plugins/plugins/mcp-server.md index ecef6503d2..f5da93c9d1 100644 --- a/guides/plugins/plugins/mcp-server.md +++ b/guides/plugins/plugins/mcp-server.md @@ -56,6 +56,7 @@ The `#[McpTool]` attribute must be on the **class**, not on `__invoke()`. Extend namespace Swag\MyPlugin\Mcp\Tool; use Mcp\Capability\Attribute\McpTool; +use Mcp\Capability\Attribute\Schema; use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository; use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria; use Shopware\Core\Framework\Mcp\Attribute\McpToolDependsOn; @@ -76,11 +77,12 @@ 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 + public function __invoke( + #[Schema(description: 'The customer\'s email address.')] + string $email, + #[Schema(description: 'How many orders to return, newest first.')] + int $limit = 10, + ): string { $context = $this->contextProvider->getContext(); if ($error = $this->requirePrivilege($context, 'order:read')) { @@ -107,12 +109,13 @@ 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`. +- Describe each parameter with `#[Schema(description: '...')]` from `Mcp\Capability\Attribute\Schema` on the `__invoke()` parameter. The MCP SDK copies the text into the parameter's `description` in `inputSchema`. A `@param` docblock works as a fallback, but core uses the attribute, and the attribute wins when both are present. 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. -- Return a `string` from `__invoke()`. The MCP SDK automatically wraps the return value into the protocol response. -- Extend `McpToolResponse` to use `$this->success()` and `$this->error()` helpers. +- Return a `string` from `__invoke()`. Extend `McpToolResponse` and return the result of `$this->success()` or `$this->error()`. Shopware turns it into the MCP result, including `structuredContent`, `isError`, and `_meta` since 6.7.16.0, and offloads results larger than 100 KB, see [Response format](../../../products/tools/mcp-server/tools-reference.md#response-format). +- Pass a stable error code as the second argument of `$this->error()`, such as `McpToolError::NOT_FOUND` or `McpToolError::INVALID_ARGUMENTS` from `Shopware\Core\Framework\Mcp\Result\McpToolError`. The default is `McpToolError::TOOL_ERROR`. This argument exists since 6.7.16.0. +- Since 6.7.16.0, a `%` in a description or schema is kept as written. Before, text between two `%` without whitespace, such as "5%-10%", was read as a container parameter and broke the MCP endpoint. If you wrote `%%` as a workaround, remove it, otherwise the description now shows `%%`. ## Step 2: Assign a tool group @@ -128,7 +131,11 @@ The attribute is optional. Without it, Shopware uses the longest hyphen-separate Because the inferred group depends on which other tools are registered without a group, declare the group explicitly when you need a stable toolset name or want a different grouping. An explicit group also takes precedence over the group Shopware derives for app tools. -The special `discovery` group places an allowed tool in the initial session surface. Such a tool is always advertised and never appears in `shopware-toolsets-list`, so it can be neither enabled nor disabled. Use it only for an intentional discovery meta-tool; regular domain tools should remain deferred. +The `discovery` group is reserved for the core discovery tools `shopware-tool-search`, `shopware-toolsets-list`, and `shopware-toolset-enable`. Do not use it for your own tools. + +Starting with Shopware 6.7.16.0, a tool that declares `#[McpToolGroup('discovery')]` is moved to the `other` toolset when the container is built. It stays callable and can be enabled, but it is no longer advertised at the start of a session. `bin/console debug:mcp` lists such tools in a warning, and the PHPStan rule `shopware.mcpReservedToolGroup` reports them. To make your tools visible on the first `tools/list`, give them a group of their own and let clients select it with `?toolsets=`, see [Select toolsets when connecting](../../../products/tools/mcp-server/getting-started.md#select-toolsets-when-connecting). + +Before 6.7.16.0, a tool in the `discovery` group was always advertised and could be neither enabled nor disabled. ## Step 3: Declare dependencies and privileges @@ -214,8 +221,12 @@ 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.16.0, `debug:mcp` also warns about tools that were moved from the reserved `discovery` group to `other`. Give such a tool a group of its own. + 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..2eb654cfeb 100644 --- a/products/tools/mcp-server/best-practices.md +++ b/products/tools/mcp-server/best-practices.md @@ -73,17 +73,17 @@ Strategies to reduce tool count within a single integration: Tool responses are injected directly into the agent's context window, so large responses consume a significant portion of the token budget. -When a response exceeds 100 KB, `McpToolResponse` does not inline it. Instead, it stores the payload in a session-scoped cache and returns a `shopware://tool-result/{uuid}` URI in `_meta.resourceUri`. The agent can fetch the full content via `resources/read`. The `_meta` block also includes `responseSize` and `note`, which explain why the result was deferred. For responses above 20 KB (but under the cap), `_meta.responseSize` is included as a size hint so the agent can learn to use tighter parameters next time. +When a response exceeds 100 KB, `McpToolResponse` does not inline it. Instead, it stores the payload in the `mcp_tool_result_cache` table and returns a `shopware://tool-result/…` URI in `_meta.resourceUri`. The agent can fetch the full content via `resources/read`. The `_meta` block also includes `responseSize` and `note`, which explain why the result was deferred, and `query` with the tool and arguments of the call. Starting with Shopware 6.7.16.0, the URI is a signed pointer that only works for the caller that produced it and expires after one hour (`_meta.expiresAt`). Clients on protocol `2025-06-18` or later also get it as a `resource_link` content block. See [Large tool results](./tools-reference.md#large-tool-results). For responses above 20 KB (but under the cap), `_meta.responseSize` is included as a size hint so the agent can learn to use tighter parameters next time. The goal is still to keep responses small. Deferred resources still cost tokens when the agent fetches them, and some clients may not automatically follow up. -- **Separate entity rows from aggregations.** Aggregations (especially `terms` or `date-histogram` with many buckets) can produce thousands of entries. Mixing them with entity rows in a single response compounds the problem — use `shopware-entity-aggregate` for aggregations and `shopware-entity-search` for records; never both in a single tool call. +- **Separate entity rows from aggregations.** Aggregations (especially `terms` or `histogram` with many buckets) can produce thousands of entries. Mixing them with entity rows in a single response compounds the problem — use `shopware-entity-aggregate` for aggregations and `shopware-entity-search` for records; never both in a single tool call. - **Use `McpEntityIncludes` in plugin tools.** If your tool returns DAL entity data via `JsonEntityEncoder`, use the `McpEntityIncludes` trait. It automatically strips unrequested associations, thumbnails, and translated duplicates, keeping responses compact without manual field filtering. - **Paginate large result sets.** Return a bounded `limit` and let the agent increment `page` rather than returning everything at once. ### Use consistent response shapes -Extend `McpToolResponse` to get consistent `{"success": true, "data": ..., "_meta": ...}` and `{"success": false, "error": "..."}` envelopes. Agents learn one pattern and apply it everywhere. +Extend `McpToolResponse` to get consistent `{"success": true, "data": ..., "_meta": ...}` and `{"success": false, "error": "...", "code": "..."}` envelopes. Agents learn one pattern and apply it everywhere. Starting with Shopware 6.7.16.0, Shopware also returns the data as `structuredContent` and sets `isError` for failed calls. Pass a stable code as the second argument of `error()`, such as `McpToolError::NOT_FOUND`, `McpToolError::INVALID_ARGUMENTS`, or `McpToolError::MISSING_PRIVILEGE`. The default is `McpToolError::TOOL_ERROR`. ### Write tool descriptions for agent routing @@ -101,6 +101,21 @@ 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 `#[Schema(description: ...)]`.** Put the attribute from `Mcp\Capability\Attribute\Schema` on each `__invoke()` parameter. The MCP SDK renders it into `inputSchema.properties[*].description`, which is where a client reads the argument contract. Without it, 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. The SDK also falls back to the `@param` docblock text, but core uses the attribute, and the attribute wins when both are present. Descriptions are compiled into the container, so run `bin/console cache:clear` after changing them. + +```php +use Mcp\Capability\Attribute\Schema; + +public function __invoke( + #[Schema(description: 'Entity name, e.g. "order".')] + string $entity, + #[Schema(description: 'A JSON ARRAY of aggregation definitions, as a string. Each element needs "name" and "type", and every type except "filter" also needs "field", e.g. [{"name":"order_count","type":"count","field":"id"}].')] + 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 with `RequestCriteriaBuilder::fromArray()` can wrap only that call, catch `ShopwareHttpException`, and return `$this->invalidCriteriaError($e)`. A `SearchRequestException` is rendered as `Invalid criteria: ` followed by every rejected element's JSON pointer and message. Other exceptions, such as an unknown association, keep their own message. The error carries the code `invalid_arguments`. Keep the actual search outside the `try`, so that real DAL failures are still logged. 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 1407863f15..f2f2a004b0 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -15,6 +15,8 @@ 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, and Shopware runs on `symfony/mcp-bundle` 0.14. See [Upgrading to 6.7.16.0](#upgrading-to-6-7-16-0). ::: ## Shopware MCP configuration @@ -24,16 +26,20 @@ Shopware-specific MCP settings live under the `shopware.mcp` key in `config/pack ```yaml shopware: mcp: - allowed_tools: [] # Empty = all tools allowed. List tool names to restrict globally. - app_tool_timeout: 10 # Timeout in seconds for app webhook tool calls. + allowed_tools: [] # Empty = all tools allowed. List tool names to restrict globally. + app_tool_timeout: 10 # Timeout in seconds for app webhook tool calls. + tool_result_cache_ttl: 86400 # Seconds a stored oversized tool result is kept (6.7.16.0 and later). ``` -These two keys are the complete `shopware.mcp` configuration: +These three keys are the complete `shopware.mcp` configuration: + +| Key | Type | Default | Description | +| ----------------------- | --------------- | ------- | -------------------------------------------------------------------------------------- | +| `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`. | +| `tool_result_cache_ttl` | integer | `86400` | Seconds a stored tool result larger than 100 KB is kept. Minimum `60`. Since 6.7.16.0. | -| Key | Type | Default | Description | -| ------------------ | --------------- | ------- | ---------------------------------------------------------------------- | -| `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`. | +The hourly `mcp_tool_result_cache.cleanup` scheduled task removes stored tool results older than `tool_result_cache_ttl`, so a result can stay up to an hour longer. Stored results can contain customer and order data. Lower the value to keep them shorter, but not below `3600`, because a link to a stored result is valid for one hour. The cleanup only runs where [scheduled tasks](../../../guides/hosting/infrastructure/scheduled-task.md) are consumed. See [Large tool results](./tools-reference.md#large-tool-results). 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). @@ -57,7 +63,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 | @@ -69,13 +76,48 @@ 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 | -`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: + +| 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. For an admin user, the switch stores no list (`null`). This only matters if the user later loses admin rights, because they then have no MCP access until capabilities are selected. Values Shopware cannot read as a list of names, such as invalid JSON or an object instead of an array, also count as nothing selected. -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. +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, open the allowlist, switch **All** on again for each affected type, and save. Saving without a change writes the `null` values back. The switch now stores the explicit list of capabilities. + +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 both privileges for users. For integrations, the **Edit MCP allowlist** permission only grants the action privilege. The role also needs the editor permission of **Settings → Integrations**, which grants `integration:update`. A custom role that only has the action privilege must be extended. ::: +The routes answer `204` on success, `404` for an unknown ID, and `400` when `allowlist` is missing, has keys other than `tools`, `resources`, and `prompts`, or a type is not a list of strings. `"allowlist": null` clears the allowlist, which for an integration or a non-admin user means nothing is allowed. There is no `GET` route. Read the `mcpAllowlist` field of the integration or user entity instead. + +Resources are listed by URI, such as `shopware://currencies`, not by name. Stored tool results under `shopware://tool-result/` are always readable by the caller that created them. + ### Delegated user calls (`sw-app-user-id`) Apps that act on behalf of a logged-in user (for example, a Copilot sidebar embedded in the Admin UI) can pass the `sw-app-user-id` header alongside integration credentials: @@ -95,15 +137,16 @@ 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 | +| `[tool-a, tool-b]` | unknown or inactive user | `[]` since 6.7.16.0, `[tool-a, 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. @@ -214,24 +257,13 @@ 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 +#### List change notifications on several servers -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'] +When an app with MCP capabilities is installed, updated, activated, deactivated, or deleted, Shopware sends `notifications/tools/list_changed`, or the prompt or resource variant, to the sessions on `/api/_mcp`. - mcp.store_api.session_registry_cache: - class: Symfony\Component\Cache\Psr16Cache - arguments: ['@cache.mcp_sessions'] -``` +Starting with 6.7.16.0, an app change increments a version per capability list in the `mcp_list_version` database table. On every request, Shopware compares these versions with the ones stored in the session and queues the notification for the lists that changed. This works on any number of servers without further configuration, as long as the session store is shared. -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 and 6.7.15.x, Shopware kept a registry of active sessions in `cache.system`, which is local to each server. Only sessions on the server that processed the app change were notified, unless `shopware.mcp.session_registry_cache` and `mcp.store_api.session_registry_cache` pointed at a shared pool and a shared [lock store](../../../guides/hosting/performance/lock-store.md) was configured. Remove those overrides when you update to 6.7.16.0. The services no longer exist, and MCP notifications no longer need a shared lock. 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. @@ -280,6 +312,8 @@ The `--tools` option is still accepted but has no effect. The `--prompts` and `- 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. +Since 6.7.16.0, `debug:mcp` also warns about plugin or bundle tools that were moved from the reserved `discovery` group to `other`. + Drill into a single capability by name: ```bash @@ -292,7 +326,11 @@ 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. +The integration filter applies to the Admin API server only. Store API tools are listed unfiltered. Starting with 6.7.16.0, the command prints this note for an integration whose allowlist grants no tools, including an unset allowlist: + +```text +Integration "": no tools allowed. Integrations have no administrator bypass, so an unset MCP allowlist grants nothing. +``` If a tool is missing from this output, it is also missing from the live endpoint. Common causes: @@ -305,12 +343,43 @@ If a tool is missing from this output, it is also missing from the live endpoint Both MCP endpoints are rate limited with their own buckets, configured under `shopware.api.rate_limiter` in `config/packages/shopware.yaml`. Rate limiting protects the endpoints from brute-force attempts and runaway agent loops. -| Bucket | Endpoint | Keyed on | Limits | -| --------------- | ----------------- | ----------------------------------------------- | ----------------------------------- | -| `mcp_admin_api` | `/api/_mcp` | Access token, falling back to client IP | 300 per minute, 1000 per 10 minutes | -| `mcp_store_api` | `/store-api/_mcp` | Sales channel and context token, plus client IP | 120 per minute, 600 per 10 minutes | +| Bucket | Endpoint | Keyed on | Limits | +| --------------- | ----------------- | ----------------------------------------------- | -------------- | +| `mcp_admin_api` | `/api/_mcp` | Access token, falling back to client IP | 300 per minute | +| `mcp_store_api` | `/store-api/_mcp` | Sales channel and context token, plus client IP | 120 per minute | + +Starting with 6.7.16.0, both use the `sliding_window` policy. A client may send at most the limit within any 60-second window, and there is no lockout. The Store API limit applies to the context and to the client IP separately. Exceeding a limit returns HTTP 429 with `MCP endpoint throttled for seconds.` in the response body; no `Retry-After` header is sent. + +To change the limits, override the buckets: + +```yaml +shopware: + api: + rate_limiter: + mcp_admin_api: + enabled: true + policy: 'sliding_window' + limit: 300 + interval: '60 seconds' + mcp_store_api: + enabled: true + policy: 'sliding_window' + limit: 120 + interval: '60 seconds' +``` + +Before 6.7.16.0, both used the `time_backoff` policy with additional 10-minute limits of 1000 and 600 requests, and reset after one hour. Once the first limit was reached, that policy accepted only one request per interval, so a busy client got HTTP 429 on almost every call. If you override these buckets, set `policy`, `limit`, and `interval`. The `reset` and `limits` keys only apply to `time_backoff`. + +## Upgrading to 6.7.16.0 + +Shopware 6.7.16.0 changes these MCP defaults: -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. +- **Allowlists:** An unset allowlist of an integration or non-admin user grants nothing. Select capabilities before or right after the update, see [Per-principal allowlist](#per-principal-allowlist). +- **Rate limiting:** `mcp_admin_api` and `mcp_store_api` use `sliding_window`. Adjust overrides of these buckets, see [Rate limiting](#rate-limiting). +- **Notifications:** The session registry services are removed. Remove overrides of `shopware.mcp.session_registry_cache` and `mcp.store_api.session_registry_cache`, see [List change notifications on several servers](#list-change-notifications-on-several-servers). +- **Discovery group:** Plugin and bundle tools in the `discovery` group are moved to `other`, see [Assign a tool group](../../../guides/plugins/plugins/mcp-server.md#step-2-assign-a-tool-group). +- **Tool results:** Results carry `structuredContent` and `isError`. Stored results use signed links that expire after one hour and are removed after `tool_result_cache_ttl`, see [Response format](./tools-reference.md#response-format). +- **MCP bundle:** Shopware requires `symfony/mcp-bundle` 0.14.1, still on `mcp/sdk` 0.8. A `%` in the metadata of a tagged tool, prompt, or resource is now kept as written. If you wrote `%%` as a workaround, remove it. App capabilities are not affected. ## Upgrading to 6.7.15.0 diff --git a/products/tools/mcp-server/copilot-tool-approval.md b/products/tools/mcp-server/copilot-tool-approval.md index 2d8dc5ee73..4491d6f12e 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: @@ -218,7 +218,7 @@ Tools that an app exposes over a webhook follow the same contract. Declare `dryR * **Stateless between calls** - Preview and commit arrive in different sessions. Never store the preview and replay it on commit. Recompute from the arguments. * **Idempotent commits where possible** - Copilot executes an approval once, but network retries can repeat a call. Design the commit so that running it twice with the same arguments is harmless. * **Privileges are the merchant's** - Copilot acts with the permissions of the merchant who is chatting. Check the privilege your change needs with `requirePrivilege()` and declare it with `McpToolRequires` so that shop administrators can configure roles. -* **Response envelope** - Always return through `success()` or `error()`. Copilot treats `success: false` as a tool error and doesn't proceed to the commit. +* **Response envelope** - Always return through `success()` or `error()`. Copilot treats `success: false` as a tool error and doesn't proceed to the commit. Starting with Shopware 6.7.16.0, the same result also carries `isError: true`. * **Response size** - Responses larger than 100 KB are returned through a resource URI instead of inline. Keep previews and results compact, so Copilot can use them directly. * **Timeouts** - Copilot waits a few seconds for a tool call. Long-running work should return quickly and continue asynchronously, reporting a reference the merchant can check. * **No preview-only side effects** - Logging is fine. Creating draft records, reserving stock, or sending notifications during a preview is not. diff --git a/products/tools/mcp-server/examples.md b/products/tools/mcp-server/examples.md index 0f76b0acc2..499b6ac125 100644 --- a/products/tools/mcp-server/examples.md +++ b/products/tools/mcp-server/examples.md @@ -234,7 +234,7 @@ Use `shopware://currencies` to find `currencyId`. Use `shopware-entity-search` o ```json [Tool: shopware-entity-aggregate] { "entity": "order", - "aggregations": "[{\"type\": \"date-histogram\", \"name\": \"ordersByMonth\", \"field\": \"orderDateTime\", \"interval\": \"month\"}]" + "aggregations": "[{\"type\": \"histogram\", \"name\": \"ordersByMonth\", \"field\": \"orderDateTime\", \"interval\": \"month\"}]" } ``` diff --git a/products/tools/mcp-server/getting-started.md b/products/tools/mcp-server/getting-started.md index 366588fcb1..fd5ce548ba 100644 --- a/products/tools/mcp-server/getting-started.md +++ b/products/tools/mcp-server/getting-started.md @@ -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 @@ -182,13 +184,18 @@ A missing, invalid, or expired token is answered with HTTP 401 and the JSON-RPC ## 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 8a7f1a4f08..935a43c177 100644 --- a/products/tools/mcp-server/index.md +++ b/products/tools/mcp-server/index.md @@ -160,6 +160,7 @@ 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, `structuredContent` in tool results, and `symfony/mcp-bundle` 0.14 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 3ebd9a1085..db6a0e6a06 100644 --- a/products/tools/mcp-server/intro.md +++ b/products/tools/mcp-server/intro.md @@ -14,22 +14,22 @@ 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. 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 moves to `symfony/mcp-bundle` 0.14, changes an unset MCP allowlist from "everything allowed" to "nothing allowed", and adds `structuredContent` and `isError` to tool results. See [Configuration](./configuration.md) for what applies to which version. ::: ## What the MCP server provides The following table summarizes the MCP server's core capabilities. -| Capability | Details | -| ------------------ | ----------------------------------------------------------------------------------------------------- | -| **HTTP endpoints** | `POST /api/_mcp` and `POST /store-api/_mcp` via Streamable HTTP transport | -| **Authentication** | Integration or OAuth credentials for Admin API; Store API authentication headers for Store API | -| **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, session-scoped toolsets, and connect-time `?toolsets` | -| **Extensibility** | Extensions can contribute custom tools, prompts, and resources | +| Capability | Details | +| ------------------ | --------------------------------------------------------------------------------------------------------------------- | +| **HTTP endpoints** | `POST /api/_mcp` and `POST /store-api/_mcp` via Streamable HTTP transport | +| **Authentication** | Integration or OAuth credentials for Admin API; Store API authentication headers for Store API | +| **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-endpoint rate limiting, keyed per access token (Admin API) or per sales-channel context and client IP (Store API) | +| **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 @@ -63,18 +63,17 @@ Capabilities are registered at container compile time from their service tags. C ## Spec coverage and known limitations -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'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.16.0 and later use `symfony/mcp-bundle ~0.14.1` with `mcp/sdk ^0.8.1`. Shopware 6.7.15.x uses `~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. -| Area | Status | -| -------------------------------------------- | ----------------------------------------------------------------------- | -| Resource subscriptions | Not implemented (resource templates are supported) | -| Completion utility for prompt/URI arguments | Not implemented | -| `structuredContent` and `isError` in results | Not used; Shopware uses its own `{"success": bool, ...}` envelope | -| ACL checks on resources | Not implemented (resources are public within the authenticated session) | +| Area | Status | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Resource subscriptions | Not implemented (resource templates are supported) | +| Completion utility for prompt/URI arguments | Not implemented | +| ACL checks on resources | Not implemented (resources are public within the authenticated session, except stored tool results, which only their caller can read) | If a feature you need is missing from `symfony/mcp-bundle`, check its [repository](https://github.com/symfony/mcp-bundle) for open issues and pending releases before building a workaround. diff --git a/products/tools/mcp-server/store-api.md b/products/tools/mcp-server/store-api.md index 1e14cce142..1a9f97b4fe 100644 --- a/products/tools/mcp-server/store-api.md +++ b/products/tools/mcp-server/store-api.md @@ -89,15 +89,17 @@ Reads the current sales-channel context for the session. It has no parameters an } ``` +Starting with Shopware 6.7.16.0, clients on protocol `2025-06-18` or later also get the same `data` object as `structuredContent`. + If the request carries no resolvable sales-channel context, the tool returns `No Store API sales-channel context is available for this MCP request.` -Core registers no prompts or resources for the Store API scope. +Core registers no prompts for the Store API scope. Starting with Shopware 6.7.16.0, it registers the `shopware://tool-result/{id}` resource template, which serves tool results larger than 100 KB. Send the same `sw-context-token` on `resources/read` as on the tool call. A call without `sw-context-token` gets its result inline. See [Large tool results](./tools-reference.md#large-tool-results). ## Sessions and rate limiting 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). +Rate limiting uses its own bucket, `mcp_store_api`: 120 requests per minute in a sliding window, keyed on the sales channel and context token, plus an additional per-client-IP limit with the same value. Before 6.7.16.0, the bucket used `time_backoff` with an additional limit of 600 requests per 10 minutes. 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). ## Advertising the endpoint diff --git a/products/tools/mcp-server/tools-reference.md b/products/tools/mcp-server/tools-reference.md index fd7085cdf6..f63f7113a8 100644 --- a/products/tools/mcp-server/tools-reference.md +++ b/products/tools/mcp-server/tools-reference.md @@ -11,7 +11,7 @@ This page documents all built-in tools, resources, and the system prompt provide ## Response format -All core tools return a consistent JSON envelope via `McpToolResponse`: +All core tools return a consistent JSON envelope via `McpToolResponse`. It is the first text block of the result: **Success:** @@ -22,14 +22,45 @@ All core tools return a consistent JSON envelope via `McpToolResponse`: **Error:** ```json -{"success": false, "error": "Human-readable message telling the agent what to do next"} +{"success": false, "error": "Human-readable message telling the agent what to do next", "code": "tool_error"} ``` The `_meta` field is optional and used for pagination context, dry-run status, and sales channel scope. +Starting with Shopware 6.7.16.0, every tool call returns a standard MCP `CallToolResult` around this text block: + +- **`structuredContent`** holds the result data, so clients don't need to parse JSON out of the text. Data that is not a JSON object, such as a list of rows, is wrapped as `{"result": …}`. A failed call has `structuredContent.error` with `code`, `message`, and, if present, `details`. +- **`isError`** is `true` for a failed call. +- **`_meta["shopware/generatedAt"]`** holds the time the result was created. + +The error `code` is stable and one of `tool_error`, `missing_privilege`, `invalid_arguments`, or `not_found`. The text block carries the same code. Clients that negotiate a protocol revision before `2025-06-18` get no `structuredContent`, because that revision introduced it. They still get `isError` and `_meta`. + +```json +{ + "content": [{"type": "text", "text": "{\"success\":false,\"error\":\"Missing privilege: order:read\",\"code\":\"missing_privilege\"}"}], + "isError": true, + "structuredContent": {"error": {"code": "missing_privilege", "message": "Missing privilege: order:read"}}, + "_meta": {"shopware/generatedAt": "2026-10-15T10:00:00+00:00"} +} +``` + +:::info Shopware 6.8 +With the `v6.8.0.0` feature flag, the first text block holds the plain data, or the error message for a failed call, instead of the `success` envelope. The summary and the metadata (`{"_meta": …}`) follow as text blocks of their own, and each metadata key is also sent as `_meta["shopware/"]`. Read `structuredContent`, `isError`, and `_meta` instead of parsing the envelope, so your client works on both. +::: + +`shopware-entity-search`, `shopware-entity-read`, `shopware-entity-aggregate`, `shopware-entity-upsert`, `shopware-entity-delete`, 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`, `shopware-entity-read`, 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\".", "code": "invalid_arguments"} +``` + +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). +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. The `discovery` group is reserved for these three tools. Starting with Shopware 6.7.16.0, a plugin or bundle tool that declares `#[McpToolGroup('discovery')]` is moved to the `other` toolset. It stays callable and can be enabled, but it is not advertised from the start. To show tools on the first `tools/list`, connect with [`?toolsets=`](./getting-started.md#select-toolsets-when-connecting). See [Assign a tool group](../../../guides/plugins/plugins/mcp-server.md#step-2-assign-a-tool-group). ### shopware-tool-search @@ -79,13 +110,22 @@ Every tool belongs to exactly one group, and every group except `discovery` beco | `theme` | Theme tools | `shopware-theme-config` (Storefront bundle) | | `store-api` | Store api tools | `shopware-store-api-context` — [Store API endpoint](./store-api.md) only | -The `discovery` group holds `shopware-tool-search`, `shopware-toolsets-list`, and `shopware-toolset-enable`. It is always advertised and never appears in `shopware-toolsets-list`, so it cannot be enabled or disabled. +The `discovery` group holds `shopware-tool-search`, `shopware-toolsets-list`, and `shopware-toolset-enable`. It is always advertised and never appears in `shopware-toolsets-list`, so it cannot be enabled or disabled. No other tool can join this group. Titles and descriptions are generated from the group name: `#[McpToolGroup('swag-my-plugin')]` produces the title "Swag my plugin tools". Extensions cannot override the generated label, so choose the group name accordingly. ## Large tool results -A tool response larger than 100 KB is not returned inline. Shopware stores it and returns a reference instead: `_meta.resourceUri` contains a `shopware://tool-result/{id}` URI that the client reads through the matching resource template. Cached results are removed when the MCP session ends. +A tool response larger than 100 KB is not returned inline. Shopware stores it and returns a reference instead: `_meta.resourceUri` contains a `shopware://tool-result/…` URI that the client reads with `resources/read` through the matching resource template. This applies to core tools and to plugin and bundle tools that extend `McpToolResponse`, on both endpoints. App tools are never offloaded. + +Starting with Shopware 6.7.16.0, the URI is a signed pointer for the caller and expires after one hour. `_meta.expiresAt` holds the expiry. Clients on protocol `2025-06-18` or later also get the URI as a `resource_link` content block. Read it on the same endpoint as the same caller: + +- On `/api/_mcp`, the same integration or user. +- On `/store-api/_mcp`, the same sales channel and `sw-context-token`. A Store API call without `sw-context-token` gets its result inline, because no later request could read it back. + +The read works on any later request of that caller, with or without an MCP session. Any other caller gets `Tool result "" not found. It may have expired or been stored by another caller.` + +Stored results are deleted when the client ends its session with `DELETE`, and otherwise by the hourly `mcp_tool_result_cache.cleanup` scheduled task after `shopware.mcp.tool_result_cache_ttl` seconds, 24 hours by default. See [Configuration](./configuration.md#shopware-mcp-configuration). Before 6.7.16.0, results were only removed when the session ended, and the Store API endpoint could not read its links back. ## Dry-run behavior @@ -131,25 +171,27 @@ Get the field and association schema of any Shopware entity. Use this first to d {"entity": "product"} ``` +The response contains `entity`, `fields` with `name`, `type`, and `required`, and `associations` with `name`, `type`, and `entity`. For a many-to-many association, `entity` is the target entity, such as `category`. Starting with Shopware 6.7.16.0, `mappingEntity` names the mapping entity, such as `product_category`, which `shopware-entity-delete` uses to remove a link. Before 6.7.16.0, `entity` named the mapping entity for many-to-many associations. + **ACL:** None (schema introspection only). ### 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. **Parameters:** -| Name | Type | Required | Default | Description | -| ---------- | ------ | -------- | ------- | --------------------- | -| `entity` | string | yes | — | Entity name | -| `criteria` | string | no | `{}` | JSON criteria object | -| `limit` | int | no | 25 | Results per page | -| `page` | int | no | 1 | Page number | -| `term` | string | no | — | Full-text search term | +| Name | Type | Required | Default | Description | +| ---------- | ------ | -------- | ------- | ------------------------------------------------ | +| `entity` | string | yes | — | Entity name | +| `criteria` | string | no | `{}` | JSON criteria object | +| `limit` | int | no | 25 | Results per page, up to `shopware.api.max_limit` | +| `page` | int | no | 1 | Page number | +| `term` | string | no | — | Full-text search term | -**Criteria supports:** `filter`, `sort`, `limit`, `page`, `associations`, `includes`, `fields`, `ids`, `term`, `query`, `post-filter`, `grouping`, `total-count-mode`. Top-level `limit`, `page`, and `term` parameters override values in criteria JSON. +**Criteria supports:** `filter`, `sort`, `limit`, `page`, `associations`, `includes`, `fields`, `ids`, `term`, `query`, `post-filter`, `grouping`, `total-count-mode`. A `limit` inside the criteria JSON takes precedence over the top-level `limit`. The top-level `page` replaces the criteria page when it is greater than 1, and a non-empty top-level `term` replaces the criteria term. **Pagination:** The response `_meta` block always includes `total`, `page`, and `limit`. Iterate by incrementing `page` until `page * limit >= total`. @@ -173,13 +215,13 @@ 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. 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. Each element needs `name` and `type`, and every type except `filter` also needs `field`. A `filter` element takes `filter` and a nested `aggregation` instead. A single object instead of an array is rejected | +| `filters` | string | no | `[]` | JSON array of filter definitions narrowing what is aggregated | -**Supported aggregation types:** `avg`, `sum`, `min`, `max`, `count`, `terms`, `date-histogram`, `range`, `filter`, `entity` +**Supported aggregation types:** `avg`, `sum`, `min`, `max`, `stats`, `count`, `terms`, `histogram` (a date histogram with `interval`), `range`, `filter`, `entity` **Response:** `{"success": true, "data": {"aggregations": {"": {...}}}}` @@ -246,21 +288,33 @@ ACL is checked per item: payloads **without** an `id` require `{entity}:create`; | `payload` | string | yes | — | JSON object or array of objects. Omit `id` to create; include `id` to update. | | `dryRun` | bool | no | `true` | Preview without persisting | +Upsert can only add many-to-many links, such as product categories. A `remove` or `delete` flag inside `categories` is written to the category itself. To remove a link, use `shopware-entity-delete` on the mapping entity. + **ACL:** `{entity}:create` and/or `{entity}:update` ### shopware-entity-delete Delete entities by their UUIDs. Returns a cascade impact preview in dry-run mode. +Starting with Shopware 6.7.16.0, the tool also removes a many-to-many link without deleting either side. Delete the row of the mapping entity, such as `product_category` or `product_property`. `shopware-entity-schema` names it as `mappingEntity`. + **Parameters:** -| Name | Type | Required | Default | Description | -| -------- | ------ | -------- | ------- | ----------------------- | -| `entity` | string | yes | — | Entity name | -| `ids` | string | yes | — | JSON array of UUIDs | -| `dryRun` | bool | no | `true` | Preview cascade effects | +| Name | Type | Required | Default | Description | +| -------- | ------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `entity` | string | yes | — | Entity name, or a mapping entity such as `product_category` | +| `ids` | string | yes | — | JSON array of UUIDs, or a comma-separated list. For a mapping entity, a JSON array of objects that name every key field, such as `[{"productId": "…", "categoryId": "…"}]` | +| `dryRun` | bool | no | `true` | Preview cascade effects | + +Each response entry contains `entity`, `ids`, and `operation`. `operation` is `delete` for deleted rows and `update` for the entities whose links changed. If only some rows don't exist, their keys are listed in `_meta.notFound`. If none exist, the call fails with `Nothing was deleted, none of these rows exist: […]`. + +**Example:** Remove a product from a category. + +```json +{"entity": "product_category", "ids": "[{\"productId\": \"\", \"categoryId\": \"\"}]", "dryRun": true} +``` -**ACL:** `{entity}:delete` +**ACL:** `{entity}:delete`, for a mapping entity such as `product_category:delete` ### shopware-system-config-write @@ -333,12 +387,12 @@ Read or update the theme configuration for a sales channel. Manages brand colors **Parameters:** -| Name | Type | Required | Default | Description | -| ---------------- | ------ | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `salesChannelId` | string | yes (validated at runtime) | `""` | Sales channel UUID. Optional in the JSON schema, so agents do not refuse the call when the user has not provided one; the tool returns an actionable error if empty. | -| `action` | string | no | `"get"` | `get` or `update` | -| `config` | string | no | `"{}"` | For `update`: JSON key-value pairs, e.g., `{"sw-color-brand-primary": {"value": "#0000ff"}}` | -| `dryRun` | bool | no | `true` | For `update`: preview without persisting | +| Name | Type | Required | Default | Description | +| ---------------- | ------ | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `salesChannelId` | string | yes (validated at runtime) | `""` | Sales channel UUID, or its name as shown in the Administration, such as "Storefront". Optional in the JSON schema, so agents do not refuse the call when the user has not provided one; the tool returns an actionable error if empty. | +| `action` | string | no | `"get"` | `get` or `update` | +| `config` | string | no | `"{}"` | For `update`: JSON key-value pairs, e.g., `{"sw-color-brand-primary": {"value": "#0000ff"}}` | +| `dryRun` | bool | no | `true` | For `update`: preview without persisting | **ACL:** `theme:read` for `get`; `theme:update` for `update`. @@ -346,16 +400,17 @@ Read or update the theme configuration for a sales channel. Manages brand colors Resources are static reference data available via MCP resource URIs. They require no tool call and do not consume tool-call budget. -| URI | Description | -| ---------------------------- | ----------------------------------------------------------------------------------------------- | -| `shopware://entities` | All registered entity names | -| `shopware://sales-channels` | All sales channels with IDs, names, types, and domains | -| `shopware://currencies` | All currencies with ISO codes, symbols, and factors | -| `shopware://languages` | All languages with locale codes | -| `shopware://state-machines` | All state machines with states and valid transitions | -| `shopware://business-events` | All events that can trigger flows | -| `shopware://flow-actions` | All flow actions available in Flow Builder | -| `shopware://extensions` | Active plugins and bundles with additional MCP tools, including tool prefix and install command | +| URI | Description | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `shopware://entities` | All registered entity names | +| `shopware://sales-channels` | All sales channels with IDs, names, types, and domains | +| `shopware://currencies` | All currencies with ISO codes, symbols, and factors | +| `shopware://languages` | All languages with locale codes | +| `shopware://state-machines` | All state machines with states and valid transitions | +| `shopware://business-events` | All events that can trigger flows | +| `shopware://flow-actions` | All flow actions available in Flow Builder | +| `shopware://extensions` | Active plugins and bundles with additional MCP tools, including tool prefix and install command | +| `shopware://tool-result/{id}` | Resource template for a stored tool result larger than 100 KB, see [Large tool results](#large-tool-results) | ## Prompts diff --git a/products/tools/mcp-server/troubleshooting.md b/products/tools/mcp-server/troubleshooting.md index a0c9c1b56d..7161a76af5 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,31 +9,37 @@ 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 | 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 | +| 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 in the effective allowlist of the integration, the user, or both | Enable it under Settings → Integrations → Edit MCP Allowlist, or on the user detail page | +| `Resource "X" is not enabled in your MCP allowlist.` | Resource not in the effective allowlist | Enable it under Settings → Integrations → Edit MCP Allowlist, or on the user detail page | +| `Prompt "X" is not enabled in your MCP allowlist.` | Prompt not in the effective allowlist | Enable it under Settings → Integrations → Edit MCP Allowlist, or on the user detail page | +| 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 | Switch **All** on again for that type, or select the capabilities, and save | +| `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, filter, 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 | Tool is not in the integration allowlist, and an unset allowlist allows nothing since 6.7.16.0 | Admin bypasses ACL only; select the tool in the integration allowlist | +| 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 with `resources/read` within one hour, with the same credentials or `sw-context-token`, or narrow the request | +| `Tool result "X" not found. It may have expired or been stored by another caller.` | Link older than one hour, read by another caller, or removed by the cleanup task | Call the tool again | +| HTTP 429 `MCP endpoint throttled for N seconds.` | Rate limit `mcp_admin_api` or `mcp_store_api` exceeded | Wait the given time, or raise the limit, see [Rate limiting](./configuration.md#rate-limiting) | +| Plugin tool no longer advertised in a fresh session after updating to 6.7.16.0 | Tool declared the reserved `discovery` group and was moved to `other` | Give the tool its own group, and connect with `?toolsets=` or enable it with `shopware-toolset-enable` | ## Connection issues @@ -120,7 +126,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**. `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). A rejected call is a JSON-RPC error with code `-32001` and HTTP status 200. The same code with HTTP 401 means an authentication problem instead. 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.