Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions guides/hosting/infrastructure/rate-limiter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions guides/plugins/apps/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
:::
Expand Down Expand Up @@ -122,6 +122,8 @@ Each `<property>` maps to a JSON Schema property in the tool's `inputSchema`:

List the ACL privileges your tool needs with `<required-privileges>`. The admin UI uses this list to warn operators when an integration's role is missing privileges a tool expects.

Every privilege in `<required-privileges>` must also be declared in the `<permissions>` 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 `<permissions>` 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.
Expand Down Expand Up @@ -271,14 +273,18 @@ 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}}
```

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
Expand All @@ -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
Expand Down
29 changes: 20 additions & 9 deletions guides/plugins/plugins/mcp-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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')) {
Expand All @@ -107,12 +109,13 @@ class MyTool extends McpToolResponse
- `title` is optional. When set, MCP clients (Claude Desktop, Cursor, etc.) display it in their tool list instead of the machine-readable `name`. Omit it if you have no better label to offer.
- Names must only contain `a-zA-Z0-9_-`.
- Parameter types on `__invoke()` are mapped to JSON schema. Supported: `string`, `int`, `float`, `bool`. Default values make parameters optional.
- Describe each parameter with a `@param` docblock on `__invoke()`. The MCP SDK copies the text into the parameter's `description` in `inputSchema`.
- Describe each parameter with `#[Schema(description: '...')]` from `Mcp\Capability\Attribute\Schema` on the `__invoke()` parameter. The MCP SDK copies the text into the parameter's `description` in `inputSchema`. A `@param` docblock works as a fallback, but core uses the attribute, and the attribute wins when both are present. See [Best Practices](../../../products/tools/mcp-server/best-practices.md#write-tool-descriptions-for-agent-routing).
- Obtain the request context via `McpContextProvider::getContext()` injected through the constructor. Do not add a `Context` parameter to `__invoke()`. The MCP SDK does not inject it there.
- `requirePrivilege()` returns an error string on failure; check its return value with `if ($error = $this->requirePrivilege(...)) { return $error; }`. `#[McpToolRequires]` is declarative only; without this call there is no runtime enforcement.
- Never use `Context::createDefaultContext()` inside a tool. It bypasses the integration's ACL. Use `McpContextProvider::getContext()` instead.
- Return a `string` from `__invoke()`. The MCP SDK automatically wraps the return value into the protocol response.
- Extend `McpToolResponse` to use `$this->success()` and `$this->error()` helpers.
- Return a `string` from `__invoke()`. Extend `McpToolResponse` and return the result of `$this->success()` or `$this->error()`. Shopware turns it into the MCP result, including `structuredContent`, `isError`, and `_meta` since 6.7.16.0, and offloads results larger than 100 KB, see [Response format](../../../products/tools/mcp-server/tools-reference.md#response-format).
- Pass a stable error code as the second argument of `$this->error()`, such as `McpToolError::NOT_FOUND` or `McpToolError::INVALID_ARGUMENTS` from `Shopware\Core\Framework\Mcp\Result\McpToolError`. The default is `McpToolError::TOOL_ERROR`. This argument exists since 6.7.16.0.
- Since 6.7.16.0, a `%` in a description or schema is kept as written. Before, text between two `%` without whitespace, such as "5%-10%", was read as a container parameter and broke the MCP endpoint. If you wrote `%%` as a workaround, remove it, otherwise the description now shows `%%`.

## Step 2: Assign a tool group

Expand All @@ -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=<group>`, 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

Expand Down Expand Up @@ -214,8 +221,12 @@ If the tool appears here, it is registered in the live HTTP endpoint and can be
- Service is tagged with `shopware.mcp.tool`
- `#[McpTool]` is on the class, not on `__invoke()`

Since Shopware 6.7.16.0, `debug:mcp` also warns about tools that were moved from the reserved `discovery` group to `other`. Give such a tool a group of its own.

Since Shopware 6.7.15.0, `debug:mcp` lists tools only. Use `bin/console debug:mcp --native` to verify prompts and resources.

Starting with Shopware 6.7.16.0, integrations and non-admin users can only call your tool after it is selected in their MCP allowlist. Tell operators which tools to select, or group them so that one toolset covers the use case.

## Adding prompts

Follow the same pattern with `#[McpPrompt]` and `shopware.mcp.prompt`:
Expand Down
Loading