Architecture overview
The shop is the server in UCP terminology. The platform is the
1. Architecture overview
1.1 System diagram
flowchart LR
subgraph Platform["Platform agent<br/>(ChatGPT / Perplexity / Gemini / custom)"]
agent[Agent runtime]
agentProfile[Platform profile<br/>JSON @ /platforms/...]
end
subgraph Shopware["Shopware shop<br/>(per Sales Channel Domain)"]
wellKnown["/.well-known/ucp<br/>WellKnownUcpController"]
ucpRoutes["/ucp/v1/* + /ucp/mcp<br/>UcpRouteScope"]
capabilities["CapabilityRegistry<br/>(DI tag: ucp.capability)"]
payments["PaymentHandlerRegistry<br/>(DI tag: ucp.payment_handler)"]
storeApi["Store-API routes<br/>(existing)"]
admin["Admin UI<br/>sw-settings-ucp"]
db[(Sales Channel<br/>UCP config + keys)]
end
agent -->|GET /.well-known/ucp| wellKnown
wellKnown -->|reads| db
wellKnown -->|consults| capabilities
wellKnown -->|consults| payments
wellKnown -->|UCP profile JSON| agent
agent -->|UCP-Agent header: profile URL| ucpRoutes
ucpRoutes -->|fetch + validate| agentProfile
ucpRoutes -->|delegate| storeApi
ucpRoutes -->|response w/ envelope| agent
admin -->|REST Admin-API| dbThe shop is the server in UCP terminology. The platform is the client. Every interaction starts with the platform reading the shop's profile to learn what it supports, after which the platform makes calls through the negotiated transport against the negotiated capabilities.
For a step-by-step explanation of the runtime handshake, signatures, OAuth, AP2 and webhooks, see Runtime flows. For exact field-level data mapping between Shopware and UCP payloads, see Shopware field mappings.
1.2 The Sales-Channel boundary
Every UCP-related state belongs to a Sales Channel. Two channels on the same Shopware install:
- have independent active toggles, capability sets, transports, signing keys
- publish independent profiles at the public
/.well-known/ucpof their domain - have independent OAuth Authorization Servers for Identity Linking
The full resolution chain on a UCP request:
flowchart TD
A[HTTP request comes in] --> B{Hostname<br/>matches a<br/>SalesChannelDomain?}
B -- no --> Z[404]
B -- yes --> C[Resolve SalesChannelContext]
C --> D{UCP active<br/>for this channel?}
D -- no --> Z
D -- yes --> E[Load ucp_sales_channel_config]
E --> F[Validate UCP-Agent header<br/>+ fetch platform profile]
F --> G[Compute capability intersection]
G --> H[Dispatch to capability controller]
H --> I[Delegate to Store-API]
I --> J[Wrap response in UCP envelope]
J --> K[200 OK + ucp.capabilities]1.3 Request lifecycle for one UCP call
sequenceDiagram
participant P as Platform
participant SW as Shopware UCP
participant Cap as CapabilityRegistry
participant API as Store-API
participant DB as Database
P->>SW: POST /ucp/v1/checkout-sessions<br/>UCP-Agent: profile="https://platform/.../profile.json"<br/>{ cart_id, … }
SW->>SW: UcpAgentRequestResolver<br/>extracts + caches platform profile
SW->>Cap: pick controller for "create_checkout"
Cap->>API: CheckoutController->create()<br/>(uses CartOrderRoute + OrderConverter)
API->>DB: write cart + checkout state
API-->>Cap: domain result
Cap->>SW: wrap in UCP envelope<br/>+ active capabilities
SW-->>P: 200 OK<br/>{ ucp: { version, capabilities, … }, id, status, … }1.4 The three registries
All three Shopware UCP registries are populated at compile time via Symfony's container, not at runtime. This makes startup faster and gives you compile-time errors if your tag attributes are wrong.
| Registry | Compiler pass | Tag | Required attributes |
|---|---|---|---|
CapabilityRegistry | UcpCapabilityCompilerPass | ucp.capability | capability (the reverse-domain name) |
UcpPaymentHandlerRegistry | UcpPaymentHandlerCompilerPass | ucp.payment_handler | handler_id (the reverse-domain name_id) |
UcpMcpToolRegistry | UcpMcpToolCompilerPass | ucp.mcp_tool | tool_name, capability |
1.5 The Negotiation algorithm
The exact algorithm runs in Negotiation/NegotiationOrchestrator.php. It
mirrors what the spec calls the Intersection Algorithm:
flowchart LR
A[Business caps] --> M{intersect by<br/>name}
P[Platform caps] --> M
M --> V{pick highest<br/>mutual version}
V -- empty --> X[exclude]
V -- non-empty --> N[include]
N --> O{has extends?}
O -- no --> R[result]
O -- yes --> O2{all parents<br/>in result?}
O2 -- yes --> R
O2 -- no --> X
X --> Q[loop: re-prune<br/>until stable]
Q --> RFor an extension with multiple parents (e.g. dev.ucp.shopping.discount
extends both cart and checkout), at least one parent needs to be
present for the extension to survive pruning — not all of them.
1.6 The envelope
Every UCP response contains a ucp envelope that lists the active
capabilities relevant to this response. Per spec, only capabilities
that match the operation type appear; the rest of the negotiated set is
omitted from the response (it stays implicit).
| Response type | Includes | Does NOT include |
|---|---|---|
| Checkout | checkout, discount, fulfillment | cart, order |
| Cart | cart, discount | checkout, fulfillment, order |
| Order | order | checkout, cart, discount |
The envelope-building logic lives in
Capability/AbstractUcpCapability.php and is shared across every
capability — you generally do not need to touch it.
→ Next: Adding a new UCP capability