From d480dcbcc1a146f7a74ce6bf86502693a1352141 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Wed, 23 Sep 2026 14:50:00 +0200 Subject: [PATCH 1/7] 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/7] 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/7] 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/7] 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 eca2ac0658684d96fcb6ff76281498b7e3352399 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Wed, 7 Oct 2026 09:23:33 +0200 Subject: [PATCH 5/7] docs(mcp): document the MCP changes of 6.7.16.0 An unset MCP allowlist grants nothing for integrations and non-admin users (shopware/shopware#20600), and the entity tools describe their parameters in inputSchema and name rejected criteria elements (shopware/shopware#20013). --- guides/plugins/plugins/mcp-server.md | 4 +- products/tools/mcp-server/best-practices.md | 13 +++++ products/tools/mcp-server/configuration.md | 56 +++++++++++++++---- .../tools/mcp-server/copilot-tool-approval.md | 2 +- products/tools/mcp-server/getting-started.md | 11 +++- 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, 118 insertions(+), 48 deletions(-) diff --git a/guides/plugins/plugins/mcp-server.md b/guides/plugins/plugins/mcp-server.md index 3516923de2..87002aaa71 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`. +- 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. @@ -212,6 +212,8 @@ 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 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 4e691b740b..59a9b75b91 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. See [Per-principal allowlist](#per-principal-allowlist). ::: ## Shopware MCP configuration @@ -57,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 | @@ -69,11 +72,42 @@ 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 | -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. +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`) @@ -95,15 +129,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 | -| --------------------- | --------------------- | ------------------- | -| `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. 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 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..e4d21b3d1e 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 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..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. 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 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 diff --git a/products/tools/mcp-server/tools-reference.md b/products/tools/mcp-server/tools-reference.md index fd7085cdf6..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). @@ -135,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. @@ -173,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. 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`, `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 c8a3b531ed..5cb0e0c48f 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,31 +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 | -| 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 | 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 @@ -120,7 +123,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). 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 6/7] 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 From d4edada75d7dd43135159a620cbe44d568df9260 Mon Sep 17 00:00:00 2001 From: BrocksiNet Date: Thu, 15 Oct 2026 09:18:58 +0200 Subject: [PATCH 7/7] docs(mcp): align the 6.7.16.0 MCP docs with trunk Cover the MCP Iteration 3 changes merged into shopware/shopware trunk: structuredContent, isError and error codes in tool results, signed resource_link pointers and the tool result cache TTL, the reserved discovery group, list change notifications without a session registry, sliding window rate limits, symfony/mcp-bundle 0.14.1, and deleting many-to-many links through the mapping entity. Correct the allowlist upgrade steps, parameter descriptions with #[Schema], the histogram aggregation type, and the MCP rate limiter rows in the hosting guide. --- guides/hosting/infrastructure/rate-limiter.md | 38 ++--- guides/plugins/apps/mcp-server.md | 14 +- guides/plugins/plugins/mcp-server.md | 27 ++-- products/tools/mcp-server/best-practices.md | 24 ++-- products/tools/mcp-server/configuration.md | 113 ++++++++++----- .../tools/mcp-server/copilot-tool-approval.md | 2 +- products/tools/mcp-server/examples.md | 2 +- products/tools/mcp-server/index.md | 2 +- products/tools/mcp-server/intro.md | 33 +++-- products/tools/mcp-server/store-api.md | 6 +- products/tools/mcp-server/tools-reference.md | 133 ++++++++++++------ products/tools/mcp-server/troubleshooting.md | 61 ++++---- 12 files changed, 280 insertions(+), 175 deletions(-) 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 e8c5f82e12..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`, see [Best Practices](../../../products/tools/mcp-server/best-practices.md#write-tool-descriptions-for-agent-routing). +- 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,6 +221,8 @@ If the tool appears here, it is registered in the live HTTP endpoint and can be - Service is tagged with `shopware.mcp.tool` - `#[McpTool]` is on the class, not on `__invoke()` +Since Shopware 6.7.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. diff --git a/products/tools/mcp-server/best-practices.md b/products/tools/mcp-server/best-practices.md index 89a877a5a0..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,18 +101,20 @@ 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. +**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 -/** - * @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 +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 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. +**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. diff --git a/products/tools/mcp-server/configuration.md b/products/tools/mcp-server/configuration.md index 227183ab9c..f2f2a004b0 100644 --- a/products/tools/mcp-server/configuration.md +++ b/products/tools/mcp-server/configuration.md @@ -16,7 +16,7 @@ Starting with 6.7.14.0, the flag is removed and has no effect. Remove `MCP_SERVE 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). +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 @@ -26,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`. | +| 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. | + +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). @@ -81,7 +85,7 @@ The allowlist is stored per capability type (`tools`, `resources`, `prompts`). A | 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. +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. A principal without any selection therefore sees the three discovery tools, and they return nothing. @@ -105,11 +109,15 @@ POST /api/_action/user/{userId}/mcp-allowlist } ``` -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. +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 `user:update`. A custom role that only has the action privilege must be extended. +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: @@ -129,13 +137,14 @@ 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 | +| --------------------- | ------------------------ | ---------------------------------------------- | +| `[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, 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. @@ -248,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: +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`. -**`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'] -``` +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. @@ -314,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 @@ -326,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: @@ -339,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 3b25e406a8..4491d6f12e 100644 --- a/products/tools/mcp-server/copilot-tool-approval.md +++ b/products/tools/mcp-server/copilot-tool-approval.md @@ -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/index.md b/products/tools/mcp-server/index.md index e4d21b3d1e..935a43c177 100644 --- a/products/tools/mcp-server/index.md +++ b/products/tools/mcp-server/index.md @@ -160,7 +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 in Shopware 6.7.16.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 5fee864d56..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. 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. 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 87c0c9ba28..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,24 +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. -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. +Starting with Shopware 6.7.16.0, every tool call returns a standard MCP `CallToolResult` around this text block: -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: +- **`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 -{"success": false, "error": "Invalid criteria: /aggregations/0/avg/field: The aggregation should contain a \"field\"."} +{ + "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 @@ -89,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 @@ -141,6 +171,8 @@ 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 @@ -151,15 +183,15 @@ Search, list, and page through records of any entity type, such as orders, produ **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`. @@ -183,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. 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. 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": {"": {...}}}}` @@ -256,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 @@ -343,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`. @@ -356,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 e87dbbaba0..7161a76af5 100644 --- a/products/tools/mcp-server/troubleshooting.md +++ b/products/tools/mcp-server/troubleshooting.md @@ -9,34 +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 | -| 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 | -| 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 @@ -123,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**. 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**. 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.