Skip to content

Extending the MCP Server via App

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. For a side-by-side comparison of all three extension types, see Extending the MCP Server.

How app capabilities work

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

StepToolsPromptsResources
Declared inmcp.xmlmcp.xmlmcp.xml
Persisted on installapp_mcp_toolapp_mcp_promptapp_mcp_resource
Loaded at runtimeAppMcpToolLoaderAppMcpPromptLoaderAppMcpResourceLoader
Executed viaHMAC-signed POSTHMAC-signed POSTHMAC-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.

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:

AttributeRequiredDescription
nameyesParameter name
typeyesJSON Schema type: string, integer, number, boolean, array, object (default: string)
descriptionnoDescription shown to the AI client
requirednoWhether 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 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:

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:

json
{
  "tool": "my-erp-sync-orders",
  "arguments": {
    "since": "2026-01-01",
    "limit": 100
 },
  "source": {
    "url": "https://shop.example.com",
    "shopId": "abc123def456",
    "appVersion": "1.2.0"
 }
}
FieldDescription
toolThe full tool name (app prefix + declared name)
argumentsThe arguments provided by the AI client
source.urlThe shop's base URL
source.shopIdUnique shop identifier. Use this for multi-tenant app backends
source.appVersionInstalled version of the app

For prompts, the request body uses prompt instead of tool, and arguments are omitted:

json
{
  "prompt": "my-erp-erp-context",
  "source": {
    "url": "https://shop.example.com",
    "shopId": "abc123def456",
    "appVersion": "1.2.0"
 }
}

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 resource instead of tool:

json
{
  "resource": "my-erp-erp-status",
  "source": {
    "url": "https://shop.example.com",
    "shopId": "abc123def456",
    "appVersion": "1.2.0"
 }
}

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 your app secret (the <secret> value from your manifest.xml). 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

    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's tool list.

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

Was this page helpful?
UnsatisfiedSatisfied
Be the first to vote!
0.0 / 5  (0 votes)