MCP-Server für WooCommerce aufbauen: ein Praktiker-Leitfaden
Model Context Protocol gibt einem KI-Agenten eine typisierte, introspektierbare Oberfläche, um auf einen Shop zu wirken. WooCommerce stellt unter /wp-json/wc/v3/ bereits eine REST-API bereit. MCP davorzusetzen verwandelt diese REST-Oberfläche in etwas, das ein LLM aufrufen kann, ohne Parameternamen zu raten. Genau diese Architektur liefere ich für Kunden aus dem deutschsprachigen Raum aus, die ihren Katalog und ihre Bestellabsicht aus Claude, ChatGPT oder einer eigenen Agenten-Runtime erreichbar machen möchten.
Dieser Artikel verankert sich am MCP-Server-Entwicklungsservice und am Universal-Commerce-Protocol-Pillar.
TL;DR
- MCP sitzt vor der WooCommerce-REST als typisierte JSON-RPC-Oberfläche.
- Drei Tools decken die meisten Agenten-Anliegen:
catalogue.list,product.detail,order.intent. - Zod-Schemata definieren jeden Input und Output und laufen bei jedem Aufruf.
- Mappe WooCommerce-Felder auf
schema.orgProduct und Offer, damit Agenten-Antworten über Oberflächen hinweg konsistent bleiben. - Cloudflare Workers ist mein Default-Deployment-Ziel für leselastige MCP-Server.
Was MCP wirklich ist
Anthropic hat Model Context Protocol am 25. November 2024 angekündigt (anthropic.com/news/model-context-protocol). Es ist ein offenes Protokoll auf JSON-RPC-2.0-Basis, das einem Client (einem LLM-Host wie Claude Desktop, einer IDE oder einer eigenen Agenten-Runtime) erlaubt, mit einem Server (deiner MCP-Implementierung) über stdio oder HTTP mit Server-Sent Events zu sprechen.
Die drei Primitive sind:
- Tools. Aufrufbare Funktionen mit typisierten Inputs und Outputs. Der Agent wählt eines aus und ruft es auf.
- Resources. Lesbare Referenzen auf Dokumente oder Datensätze, die der Agent in den Kontext ziehen kann.
- Prompts. Vom Server gelieferte Prompt-Vorlagen, die der Host invocieren kann.
Für einen WooCommerce-Shop tragen Tools die Hauptlast. Katalog durchsuchen ist ein Tool-Aufruf. Produktdetail ist ein Tool-Aufruf. Eine Bestellung vorschlagen ist ein Tool-Aufruf. Resources helfen, wenn der Agent den vollständigen Text einer Rückgabebedingung oder eine lange Produktbeschreibung braucht; Prompts helfen, wenn voreingestellte Interaktionen ausgeliefert werden sollen (“empfehle drei Produkte für dieses Kundenprofil”).
Das Intent-Inventar steht zuerst
Vor jeder Codezeile schreibe ich auf, welche Intents ein Agent gegenüber dem Shop ausdrücken können soll. Bei einem typischen WooCommerce-Build ist die Liste kurz:
- Katalog mit Filtern durchsuchen (
catalogue.list). - Vollständiges Produktdetail abrufen (
product.detail). - Eine Bestellung im Auftrag eines Kunden vorschlagen, die als Entwurf zur menschlichen Bestätigung zurückkommt (
order.intent). - Status einer bestehenden Bestellung über Bestellnummer und E-Mail abfragen (
order.status). - Bestand für eine SKU prüfen (
inventory.check).
Jedes Intent wird auf genau ein Tool abgebildet. Widerstehe der Versuchung, wc.products.get, wc.products.list, wc.orders.create als 1:1-Wrap der REST-Oberfläche zu exponieren. Dem Agenten helfen REST-Verben nicht; ihm helfen Intent-Verben.
Tool-Definitionen mit Zod
Das TypeScript-SDK unter @modelcontextprotocol/sdk nutzt Zod (zod.dev) für Input- und Output-Schemata. So sieht der Vertrag für catalogue.list aus:
import { z } from "zod";
export const catalogueListInput = z.object({
query: z.string().min(2).max(200).optional(),
category: z.string().optional(),
in_stock: z.boolean().optional(),
price_min: z.number().nonnegative().optional(),
price_max: z.number().nonnegative().optional(),
page: z.number().int().min(1).max(100).default(1),
per_page: z.number().int().min(1).max(50).default(12),
});
export const catalogueListOutput = z.object({
results: z.array(
z.object({
sku: z.string(),
name: z.string(),
url: z.string().url(),
price: z.object({
amount: z.number().nonnegative(),
currency: z.string().length(3),
}),
availability: z.enum(["InStock", "OutOfStock", "PreOrder"]),
image: z.string().url().optional(),
})
),
total: z.number().int().nonnegative(),
page: z.number().int(),
});
Zwei Dinge sind hier entscheidend. Erstens nutzt der Output schema.org-Vokabular (InStock, OutOfStock, PreOrder sind direkte Werte aus ItemAvailability). Zweitens weist der Input Müll an der Protokollgrenze ab; der Handler sieht nie ein query: "" oder ein negatives price_min.
Ich deklariere jedes Tool nach demselben Muster: Input-Schema, Output-Schema, Handler. Der Handler ist ein dünner Adapter über /wp-json/wc/v3/. Zod läuft pro Aufruf zweimal: einmal auf Input, einmal auf Output. Der Output-Check fängt Handler-Bugs ab, die sonst fehlerhafte Daten an den Agenten durchreichen würden.
Brücke zur WooCommerce-REST-API
Der Handler für catalogue.list liest aus /wp-json/wc/v3/products:
async function handleCatalogueList(input: z.infer<typeof catalogueListInput>) {
const params = new URLSearchParams();
if (input.query) params.set("search", input.query);
if (input.category) params.set("category", input.category);
if (input.in_stock) params.set("stock_status", "instock");
if (input.price_min !== undefined) params.set("min_price", String(input.price_min));
if (input.price_max !== undefined) params.set("max_price", String(input.price_max));
params.set("page", String(input.page));
params.set("per_page", String(input.per_page));
const response = await fetch(
`${WC_BASE}/wp-json/wc/v3/products?${params.toString()}`,
{ headers: { Authorization: `Basic ${WC_AUTH}` } }
);
const products = await response.json();
const total = Number(response.headers.get("X-WP-Total") ?? 0);
return catalogueListOutput.parse({
results: products.map(mapWooProductToSchemaOrg),
total,
page: input.page,
});
}
Der Helper mapWooProductToSchemaOrg übersetzt WooCommerce-Feldnamen (stock_status, regular_price, permalink) in die schema.org-konforme Form aus dem Output-Schema. Field-Mapping an einer Stelle, aufgerufen aus einem Tool, aufgerufen aus einem Handler. Drift zwischen WooCommerce-Upgrades und Agenten-Vertrag fängt der Output-parse-Aufruf ab, nicht die Produktion.
Bei order.intent rufe ich aus dem Handler nie WooCommerce-Schreibendpunkte auf. Das Tool gibt einen Bestellentwurf zurück, den der Host der Nutzerin anzeigt, die Nutzerin bestätigt, und erst dann ruft ein separater authentifizierter Pfad POST /wp-json/wc/v3/orders auf. Auf Verdacht vom LLM aus zu schreiben ist der falsche Default. Ein DACH-Shop hat zudem mit der Drei-Klick-Lösung nach BGB §312j eine zusätzliche Pflicht zur Bestätigung; der Draft-Pfad spielt da sauber rein.
schema.org-Abgleich ist tragend
Zwei Gründe, Tool-Outputs am schema.org-Vokabular auszurichten:
Agenten benutzen dieselben Wörter über Quellen hinweg. Wenn ein Agent “ist dieses Produkt verfügbar?” beantwortet, näht er eine Product-Entität aus deinem MCP-Server, ein Offer mit availability und gegebenenfalls eine Review zusammen. Liefert dein MCP-Server { "stock": "ja" } und die andere Quelle availability: "https://schema.org/InStock", muss der Agent zwei Vokabulare versöhnen. Mit schema.org-Ausrichtung auf deiner Seite muss er das nicht.
Die strukturierten Daten deines Shops sprechen ohnehin schema.org. Dein /wp-content/themes/<theme>/single-product.php (oder das Headless-Pendant) gibt Product-JSON-LD aus. Wenn der MCP-Output dieselbe Form behält, liefert dieselbe Produktseite dieselben Daten an einen Google-Crawler, einen OpenAI-Crawler über die URL und einen Agenten, der dein MCP-Tool direkt aufruft.
Das Mapping für ein typisches WooCommerce-Produkt sieht so aus:
| WooCommerce-Feld | schema.org-Feld |
|---|---|
sku | sku |
name | name |
permalink | url |
regular_price + currency | offers.price + priceCurrency |
stock_status: "instock" | availability: InStock |
stock_status: "outofstock" | availability: OutOfStock |
images[0].src | image |
description | description |
Variationen brauchen eine zusätzliche Schicht (hasVariant-Array von Product), die Grundform bleibt aber gleich.
Idempotenz und Seiteneffekte
Lese-Tools (catalogue.list, product.detail, inventory.check, order.status) sind natürlich idempotent. Drei Aufrufe liefern dieselbe Antwort.
order.intent ist die Falle. Der Agent kann zweimal aufrufen, wenn das Netz zwischen Antwort und Host kurz wackelt. Der Vertrag, den ich ausliefere:
order.intentakzeptiert einen optionalenidempotency_key(eine vom Host gelieferte UUID).- Der Handler speichert den Schlüssel in Workers KV mit 24-Stunden-TTL zusammen mit der ID des erzeugten Bestellentwurfs.
- Ein Wiederholungsaufruf mit demselben Schlüssel liefert denselben Entwurf zurück, statt einen neuen zu erzeugen.
Dieses Muster entspricht der Idempotenz-Behandlung von Stripe bei POST /v1/charges. Es ist gut verstanden; Agenten und Menschen profitieren gleichermaßen.
Deployment auf Cloudflare Workers
Das MCP-TypeScript-SDK exportiert eine Server-Klasse plus Transport-Adapter. Für Workers nutze ich den streambaren HTTP-Transport. Der Worker-Einstieg sieht so aus:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const auth = request.headers.get("Authorization");
if (!isValidAgentToken(auth, env)) {
return new Response("Unauthorized", { status: 401 });
}
const server = createWooCommerceMcpServer(env);
const transport = new StreamableHTTPServerTransport(request);
return server.connect(transport);
},
};
isValidAgentToken prüft gegen scoped Tokens in Workers KV. Read-only-Tools laufen mit einem weniger privilegierten Token; order.intent verlangt ein Token mit Scope orders:write. Die Auth-Strategie ist eines der beiden Muster aus dem Leitfaden zu MCP-Authentifizierungsmustern.
wrangler.toml deklariert die WordPress-Origin-URL, den WooCommerce-Consumer-Key und das Secret (als Worker-Secrets, niemals im Code) und den KV-Namespace für Idempotenz. Deployen via wrangler deploy. Der Worker bleibt klein (deutlich unter dem 1-MB-Bundle-Limit), weil er nur JSON-RPC-Routing plus REST-Adapter ist.
Observability ist nicht optional
Jeder Tool-Aufruf wird geloggt mit:
- Tool-Name.
- Einem SHA-256-Hash des Inputs (nicht der Input selbst; PII-Risiko nach DSGVO Art. 32).
- Latenz.
- Ob die Output-Validierung bestanden oder gescheitert ist.
- Scope des Agenten-Tokens.
Die Logs gehen via Cloudflare Logpush in einen Langzeit-Store. Zwei operative Gewohnheiten, die das ermöglicht:
Schema-Verschärfung. Sechs Wochen nach Launch sehe ich die Validierungsfehler durch. Jeder Output-Fehler ist ein Handler-Bug oder ein nicht gemapptes WooCommerce-Feld. Jeder Input-Fehler ist entweder ein fehlerhafter Agent oder ein zu strenges Schema. Beides wandert in den nächsten Release.
Kostenzuordnung. Tool-Aufrufe sind abrechenbare Last auf dem WooCommerce-Origin. Das Log zeigt, welches Agenten-Token welche Last und welches Tool erzeugt. Ein einzelner fehlverhaltender Agent, der auf catalogue.list looped, ist eine Log-Abfrage von der Identifizierung entfernt.
Wo das im Cluster sitzt
Dieser Artikel ist der Implementierungs-Durchstich. Für die Tiefe zu typisierten Tools siehe typisierte Katalog-Tools mit Zod für MCP. Für die Auth-Muster siehe MCP-Authentifizierungsmuster: OAuth, Tokens und wann was. Für die Protokoll-Entscheidung zwischen MCP und REST siehe MCP vs REST: wann was gewinnt für KI-Agenten-Integration. Für den Migrationspfad von einer bestehenden API siehe bestehende WordPress-API auf MCP migrieren: 4-Wochen-Playbook. Der Pillar ist MCP-Server-Entwicklung, die übergeordnete Strategie liegt unter Universal Commerce Protocol.
Preisgestaltung individuell, weil der Umfang davon abhängt, wie viele Tools du brauchst, wie komplex die WooCommerce-Erweiterungen sind und welche Agenten-Runtimes du unterstützen willst.
