---
url: /docs/guides/plugins/apps/mcp-server.md
---

# Extending the MCP Server via App

Shopware apps can add custom tools, prompts, and resources to the MCP server through a declarative `Resources/mcp.xml` file. When an AI client calls an app tool, Shopware sends an HMAC-signed HTTP POST to your app's endpoint and returns the response to the client.

Use an app when:

* Your capability runs on a remote service (ERP, PIM, CRM, SaaS backend)
* You want cloud compatibility (apps work in Shopware Cloud environments where plugins cannot run)
* Your capability needs to scale or deploy independently from Shopware
* You are building a SaaS integration where isolation matters more than in-process performance

For in-process PHP with full DAL access, see [Extending via Plugin](../plugins/mcp-server.md). For a side-by-side comparison of all three extension types, see [Extending the MCP Server](../../../products/tools/mcp-server/extending.md).

## How app capabilities work

```mermaid
sequenceDiagram
 participant Client as AI Client
 participant SW as Shopware
 participant App as Your App
 Client->>SW: call tool
 SW->>App: HMAC-signed POST
 App-->>SW: JSON response
 SW-->>Client: result
```

All three capability types (tools, prompts, resources) follow the same lifecycle:

| Step                 | Tools              | Prompts              | Resources              |
| -------------------- | ------------------ | -------------------- | ---------------------- |
| Declared in          | `mcp.xml`          | `mcp.xml`            | `mcp.xml`              |
| Persisted on install | `app_mcp_tool`     | `app_mcp_prompt`     | `app_mcp_resource`     |
| Loaded at runtime    | `AppMcpToolLoader` | `AppMcpPromptLoader` | `AppMcpResourceLoader` |
| Executed via         | HMAC-signed POST   | HMAC-signed POST     | HMAC-signed POST       |

## Naming convention

Names declared in `mcp.xml` are automatically prefixed with the app name at install time:

```text
app name: "my-erp"
declared name: "sync-orders"
→ final tool name: "my-erp-sync-orders"
```

Names must only contain `a-zA-Z0-9_-` (no dots). The `shopware-` prefix is reserved for core tools; app names that would produce a `shopware-` prefixed capability are silently skipped.

## Tool groups and discovery

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.

:::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).

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.
:::

## `mcp.xml` structure

Place `Resources/mcp.xml` in your app bundle:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<mcp xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Mcp/Schema/mcp-1.0.xsd">

    <mcp-tools>
        <mcp-tool name="sync-orders" url="https://app.example.com/mcp/sync-orders">
            <label>Sync Orders</label>
            <label lang="de-DE">Bestellungen synchronisieren</label>
            <description>Synchronize orders with the external ERP system</description>
            <input-schema>
                <property name="since" type="string" description="ISO 8601 date, sync orders after this date" required="true"/>
                <property name="limit" type="integer" description="Maximum number of orders to sync" required="false"/>
            </input-schema>
            <required-privileges>
                <privilege>order:read</privilege>
                <privilege>order:update</privilege>
            </required-privileges>
        </mcp-tool>
    </mcp-tools>

    <mcp-prompts>
        <mcp-prompt name="erp-context" url="https://app.example.com/mcp/prompts/erp-context">
            <label>ERP Context</label>
            <description>Background context for working with ERP-synced data</description>
        </mcp-prompt>
    </mcp-prompts>

    <mcp-resources>
        <mcp-resource name="erp-status" uri="my-erp://erp-status"
            url="https://app.example.com/mcp/resources/erp-status" mime-type="application/json">
            <label>ERP Status</label>
            <description>Current ERP connection status and last sync timestamp</description>
        </mcp-resource>
    </mcp-resources>

</mcp>
```

### Input schema properties

Each `<property>` maps to a JSON Schema property in the tool's `inputSchema`:

| Attribute     | Required | Description                                                                                       |
| ------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `name`        | yes      | Parameter name                                                                                    |
| `type`        | no       | JSON Schema type: `string`, `integer`, `number`, `boolean`, `array`, `object` (default: `string`) |
| `description` | no       | Description shown to the AI client                                                                |
| `required`    | no       | Whether the parameter is required (default: `false`)                                              |

### Required privileges

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.

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.
* **Internal path** (`/api/...`): enforced by Shopware. The call is dispatched as a Symfony subrequest via the normal Admin API stack, so the integration's ACL role is checked just as with any other API call. Declare the privileges accurately so the Admin UI can warn operators about gaps.

## URL modes: external vs internal

The `url` attribute supports two modes:

* **External URL** (`https://...`): Shopware sends an HMAC-signed POST to your remote endpoint. This is the default for SaaS integrations and any app hosted outside Shopware.
* **Internal path** (`/...`): Shopware dispatches the call as a Symfony subrequest. This lets an app use [app scripts](./app-scripts/) to serve capability logic without running an external server. Example: `url="/api/script/mcp-greet"`.

Use the internal-path mode when your tool is a thin wrapper around data that Shopware already has. Use external URLs when you need to call out to an ERP, PIM, or any system that Shopware cannot reach directly.

### Internal-path example: app script

Point the tool at an [app script endpoint](./app-scripts/add-api-endpoint.md):

```xml
<mcp-tool name="greet" url="/api/script/mcp-greet">
    <label>Greet</label>
    <description>Return a greeting for the given name.</description>
    <input-schema>
        <property name="name" type="string" description="Name to greet" required="true"/>
    </input-schema>
</mcp-tool>
```

In `Resources/scripts/api-mcp-greet/greet.twig`, access the call's `arguments` directly through `hook.request`:

```twig
{% block response %}
 {% set args = hook.request.arguments ?? {} %}
 {% set name = args.name ?? 'World' %}

 {% set response = services.response.json({
 success: true,
 data: { message: 'Hello, ' ~ name ~ '!' }
 }) %}

 {% do hook.setResponse(response) %}
{% endblock %}
```

`hook.request` exposes the parsed POST body. Arguments live under `hook.request.arguments`. Shopware handles signature verification and locale resolution for internal-path calls. You only need to return the JSON envelope.

## Webhook protocol

When the AI client calls a tool, Shopware sends an HTTP POST to the URL declared in `mcp.xml`:

**Request body:**

The following tool, prompt, and resource request bodies are excerpts that omit `source.appVersion`.

```json
{
  "tool": "my-erp-sync-orders",
  "arguments": {
    "since": "2026-01-01",
    "limit": 100
 },
  "source": {
    "url": "https://shop.example.com",
    "shopId": "abc123def456"
 }
}
```

| Field               | Description                                                                            |
| ------------------- | -------------------------------------------------------------------------------------- |
| `tool`              | The full tool name (app prefix + declared name)                                        |
| `arguments`         | The arguments provided by the AI client                                                |
| `source.url`        | The shop's base URL                                                                    |
| `source.shopId`     | Unique shop identifier. Use this for multi-tenant app backends                         |
| `source.appVersion` | Installed app version for external tools; `"0.0.0"` for external prompts and resources |

For external webhooks, use `source.appVersion` when tool handling depends on the installed app version; prompts and resources receive the placeholder `"0.0.0"`.

For prompts, the request body uses `tool` for the prompt name and includes an empty `arguments` array:

```json
{
  "tool": "my-erp-erp-context",
  "arguments": [],
  "source": {
    "url": "https://shop.example.com",
    "shopId": "abc123def456"
 }
}
```

The response must be a JSON array of message objects:

```json
[
 {"role": "user", "content": "You are working with ERP-synced Shopware data..."}
]
```

For resources, the request body uses `tool` for the resource name and includes the resource URI in `arguments`:

```json
{
  "tool": "my-erp-erp-status",
  "arguments": {
    "uri": "my-erp://erp-status"
  },
  "source": {
    "url": "https://shop.example.com",
    "shopId": "abc123def456"
 }
}
```

The response must be a single object with `uri`, `mimeType`, and `text`:

```json
{"uri": "my-erp://erp-status", "mimeType": "application/json", "text": "{\"connected\": true}"}
```

## Verifying the signature

Every webhook request is signed with HMAC-SHA256 using the shop-specific secret exchanged during app registration. Always verify the `shopware-shop-signature` header before processing the request:

```javascript
// Node.js example
const crypto = require('crypto');

function verifySignature(body, signature, appSecret) {
    if (typeof signature !== 'string' || signature.length === 0) return false;
    const expected = crypto
 .createHmac('sha256', appSecret)
 .update(body)
 .digest();
    const received = Buffer.from(signature, 'hex');
    if (received.length !== expected.length) return false;
    return crypto.timingSafeEqual(received, expected);
}

app.post('/mcp/sync-orders', (req, res) => {
    const signature = req.headers['shopware-shop-signature'];
    const rawBody = req.rawBody; // unparsed request body string

    // Set APP_SECRET to the shop-specific secret exchanged during app registration.
    if (!verifySignature(rawBody, signature, process.env.APP_SECRET)) {
        return res.status(401).json({ error: 'Invalid signature' });
 }

    const { arguments: args, source } = req.body;
    // ... handle the tool call
});
```

## Returning a response

Return JSON as the tool result body. Shopware forwards it to the AI client as-is:

```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.

**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

Labels and descriptions in `mcp.xml` support translations via `lang` attributes. Shopware resolves them against the system's default locale at load time. If a translation for the active locale is not found, it falls back to `en-GB`.

The resolved `<label>` value is forwarded to the MCP protocol as the capability's `title` field. MCP clients (Claude Desktop, Cursor, etc.) display this title in their tool list instead of the machine-readable `name`. Without a `<label>`, clients fall back to showing `name`.

## Installing the app

Apps are installed through the standard Shopware app lifecycle (**Settings → Extensions → My Apps**, or via the **Shopware CLI**). After installation, your MCP capabilities appear automatically in the server catalogue. Tools are initially deferred until the client discovers them or enables the app's toolset.

Verify with:

```bash
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.

## Further reading

* [MCP Concepts](../../../products/tools/mcp-server/mcp-concepts.md): tools, resources, and prompts explained
* [Best Practices](../../../products/tools/mcp-server/best-practices.md): design principles for MCP tools
* [Extending via Plugin](../plugins/mcp-server.md): in-process PHP alternative
* [McpHelloWorld](https://github.com/shopwareLabs/McpHelloWorld): minimal example app registering tools, prompts, and resources via webhook
