Typisierte Katalog-Tools mit Zod für MCP

Typisierte Katalog-Tools mit Zod für MCP

Aktualisiert: 23. Oktober 2026
7 Min. Lesezeit
Leitfaden
500+ WP-Projekte
KI-Integration

Ausführlichere und aktuelle Fassung dieses Themas: MCP-Server für WooCommerce aufbauen: ein Praktiker-Leitfaden.

#Typisierte Katalog-Tools mit Zod für MCP

Ein typisierter Vertrag ist der Unterschied zwischen einem Agenten, der zuverlässig Ihren Shop aufruft, und einem, der Parameternamen halluziniert. Zod (zod.dev) ist die Schema-Bibliothek, die das offizielle @modelcontextprotocol/sdk nutzt; sie liefert Laufzeit-Validierung, JSON-Schema-Generierung für tools/list und TypeScript-Typinferenz aus einer einzigen Quelle. Genau dieses Muster nutze ich für jedes MCP-Tool, das ich ausliefere.

Dieser Artikel verankert sich am Pillar MCP-Server-Entwicklungsservice.

#TL;DR

  • Schreiben Sie Input- und Output-Schemata in Zod; lassen Sie z.infer die Handler-Signatur ableiten.
  • Nutzen Sie .strict(), sodass unbekannte Schlüssel früh an der Protokollgrenze scheitern.
  • Spiegeln Sie schema.org-Vokabular in Outputs, wo ein passender Typ existiert.
  • Mutierende Tools nehmen einen optionalen idempotency_key; memoisieren Sie per Schlüssel in Workers KV.
  • Tool-Level-Fehler-Envelope für Domänenfehler; JSON-RPC-Fehler für Protokoll-Fehler.

#Wie benennt man MCP-Tools nach Best Practice?

Tool-Namen sind der öffentliche Vertrag mit jedem Agenten, der je Ihren Server aufruft. Sie sollen sich wie Verb-Objekt-Intents lesen, nicht wie REST-Endpunkte:

  • Gut: catalogue.list, product.detail, order.intent, inventory.check, order.status.
  • Schlecht: wc_products_get, getProductById, searchCatalog.

Sobald ein Agent gegen catalogue.list gebaut wurde, bricht eine Umbenennung in catalog.list jeden Konsumenten still, weil der Agent das Tool nicht mehr findet. Behandeln Sie den Namen wie ein Release-Versprechen.

Die Tool-Beschreibung zählt so viel wie der Name, denn ein Agent liest Namen und Beschreibungen in jedem Schritt neu und wählt allein danach ein Tool aus. Luca Rossi empfiehlt im Oktober 2026 in seinem Essay How to design a good MCP server, die Beschreibung für Einschränkungen, Vorbedingungen und die Fälle zu nutzen, in denen das Tool nicht passt, und Änderungen daran wie Code zu prüfen. Der MCP-Server von Sentry zeigt das Muster: Die Beschreibung von search_errors endet mit einem Satz, der Lognachrichten, Anfragen und Latenz sowie gruppierte Fehlerlisten ausschließt und für jeden Fall das passende Tool nennt. Auf einem Katalogserver würde derselbe Satz in catalogue.list Fragen zum Bestand einer bekannten SKU an inventory.check verweisen und den vollständigen Datensatz eines Produkts an product.detail.

#Strikte Zod-Input-Schemata für MCP-Tools

Mein Default für jedes Input-Schema:

import { z } from "zod";

export const catalogueListInput = z
  .object({
    query: z.string().min(2).max(200).optional()
      .describe("Freitextsuche über Produktname und SKU"),
    category: z.string().optional()
      .describe("Kategorien-Slug, wie er in WooCommerce erscheint"),
    in_stock: z.boolean().optional()
      .describe("Auf aktuell vorrätige Produkte einschränken"),
    price_min: z.number().nonnegative().optional()
      .describe("Untere Grenze des regulären Preises, inklusive"),
    price_max: z.number().nonnegative().optional()
      .describe("Obere Grenze des regulären Preises, inklusive"),
    page: z.number().int().min(1).max(100).default(1)
      .describe("Seitennummer, 1-basiert"),
    per_page: z.number().int().min(1).max(50).default(12)
      .describe("Ergebnisse pro Seite, max 50"),
  })
  .strict()
  .refine(
    (v) => v.price_min === undefined || v.price_max === undefined || v.price_min <= v.price_max,
    { message: "price_min muss kleiner oder gleich price_max sein" }
  );

Drei Dinge, die hervorzuheben sind:

.describe() überall. Das MCP-SDK leitet die Description in das JSON Schema weiter, das tools/list zurückgibt. Der Agent liest sie. Ein Feld ohne Description ist ein Feld, das der Agent raten muss; ein Feld mit Description ist ein Feld, das der Agent richtig trifft.

.strict() am Objekt. Unbekannte Schlüssel scheitern in der Validierung. Ein Agent, der { query: "stiefel", katgorie: "herren" } (Tippfehler) sendet, bekommt einen klaren Validierungsfehler auf katgorie, statt eines erfolgreichen Aufrufs, der den verschriebenen Filter still ignoriert.

.refine() für feldübergreifende Regeln. Alles, was von mehr als einem Feld abhängt, gehört in ein .refine(). Die Preis-Range-Bedingung ist das kanonische Beispiel; “nur eines von” ist das andere.

#Zod-Output-Schema nach schema.org Product

Das Output-Schema erledigt zwei Aufgaben: es dokumentiert die Antwortform für den Agenten und es validiert den Rückgabewert des Handlers, bevor er den Server verlässt.

export const productSchemaOrgShape = z.object({
  "@type": z.literal("Product"),
  sku: z.string(),
  name: z.string(),
  url: z.string().url(),
  description: z.string(),
  image: z.string().url().optional(),
  brand: z.object({ "@type": z.literal("Brand"), name: z.string() }).optional(),
  offers: z.object({
    "@type": z.literal("Offer"),
    price: z.number().nonnegative(),
    priceCurrency: z.string().length(3),
    availability: z.enum([
      "https://schema.org/InStock",
      "https://schema.org/OutOfStock",
      "https://schema.org/PreOrder",
    ]),
    url: z.string().url(),
  }),
});

export const catalogueListOutput = z.object({
  results: z.array(productSchemaOrgShape),
  total: z.number().int().nonnegative(),
  page: z.number().int().min(1),
  per_page: z.number().int().min(1),
});

Zwei Gründe, schema.org explizit zu spiegeln:

Dasselbe Vokabular über Oberflächen hinweg. Ihr Storefront emittiert Product-JSON-LD auf der Produktdetailseite. Ihr MCP-product.detail-Tool liefert dieselbe Form. Ein Agent, der Kontext aus “der Seite, die ich auf /shop/widget gecrawlt habe” und “was Ihr MCP-Server mir sagte” zusammennäht, sieht ein Vokabular, nicht zwei.

Stabile IDs in availability. https://schema.org/InStock ist eine URL; sie ist permanent, maschinenlesbar und eindeutig. Ein freier String wie "ja" oder "in stock" ist ein Übersetzungsproblem.

Der Handler ruft parse beim Verlassen auf:

return catalogueListOutput.parse({
  results: products.map(mapWooProductToSchemaOrgProduct),
  total,
  page: input.page,
  per_page: input.per_page,
});

Liefert das WooCommerce-Mapping ein malformes Product, wirft parse und der Fehler wird stromaufwärts gefangen. Der Agent sieht die malformen Daten nie; der Entwickler sieht einen typisierten Validierungsfehler im Log.

#Idempotenz-Schlüssel für schreibende MCP-Tools

Lese-Tools sind natürlich idempotent. Mutierende Tools brauchen einen expliziten Schlüssel. Das Muster, das Stripe etabliert hat, ist das, dem ich folge (Stripe-Idempotenz-Referenz):

export const orderIntentInput = z
  .object({
    customer: z.object({
      email: z.string().email(),
      name: z.string().min(1).max(200),
    }),
    items: z
      .array(
        z.object({
          sku: z.string(),
          quantity: z.number().int().min(1).max(999),
        })
      )
      .min(1)
      .max(50),
    shipping_method: z.string().optional(),
    notes: z.string().max(1000).optional(),
    idempotency_key: z.string().uuid().optional()
      .describe("Optionale UUID. Wiederholte Aufrufe mit demselben Schlüssel liefern denselben Bestellentwurf."),
  })
  .strict();

Der Handler:

async function handleOrderIntent(
  input: z.infer<typeof orderIntentInput>,
  env: Env,
): Promise<OrderIntentOutput> {
  if (input.idempotency_key) {
    const cached = await env.IDEMPOTENCY.get(`oi:${input.idempotency_key}`, "json");
    if (cached) return orderIntentOutput.parse(cached);
  }

  const draft = await createDraftOrder(input, env);

  if (input.idempotency_key) {
    await env.IDEMPOTENCY.put(
      `oi:${input.idempotency_key}`,
      JSON.stringify(draft),
      { expirationTtl: 60 * 60 * 24 }
    );
  }

  return orderIntentOutput.parse(draft);
}

24 Stunden ist die Default-TTL, weil das lang genug für einen Agenten-Retry-Sturm ist und kurz genug, dass KV klein bleibt.

#Protokollfehler und Domänenfehler in MCP-Tools

Zwei Fehlerarten, zwei Transporte:

Protokoll-Fehler. Method not found, invalid params, parse error. Diese reisen als JSON-RPC-Fehlerantworten mit Codes -32700 bis -32603 (JSON-RPC-Spec). Das MCP-SDK behandelt sie automatisch, wenn die Zod-Input-Validierung scheitert.

Domänenfehler. Nicht auf Lager, Kunde nicht gefunden, Umfang unzureichend. Diese reisen innerhalb einer erfolgreichen Tool-Antwort, in einem strukturierten error-Feld. Der Agent behandelt sie als Daten, räsoniert darüber und entscheidet als Nächstes.

Das Envelope, das ich nutze:

export const toolError = z.object({
  code: z.enum([
    "out_of_stock",
    "not_found",
    "forbidden",
    "rate_limited",
    "internal",
  ]),
  message: z.string(),
  retry_after_seconds: z.number().int().nonnegative().optional(),
  fields: z.record(z.string(), z.unknown()).optional(),
});

export const orderIntentOutput = z.discriminatedUnion("status", [
  z.object({
    status: z.literal("ok"),
    draft_order_id: z.string(),
    total: z.number().nonnegative(),
    currency: z.string().length(3),
    estimated_delivery: z.string().datetime().optional(),
  }),
  z.object({
    status: z.literal("error"),
    error: toolError,
  }),
]);

Die Discriminated Union zwingt den Agenten, status zu prüfen, bevor er irgendein anderes Feld liest. Eine “erfolgreiche Antwort mit leerem Entwurf” geht den Typcheck nie durch; entweder liefern Sie status: "ok" mit den Bestelldaten oder status: "error" mit dem Envelope.

#TypeScript-Typen aus Zod-Schemata mit z.infer

Schemata sind die einzige Wahrheit. Typen kommen via z.infer:

type CatalogueListInput = z.infer<typeof catalogueListInput>;
type CatalogueListOutput = z.infer<typeof catalogueListOutput>;
type OrderIntentInput = z.infer<typeof orderIntentInput>;
type OrderIntentOutput = z.infer<typeof orderIntentOutput>;

Die Handler-Signatur nutzt diese direkt:

async function handleCatalogueList(
  input: CatalogueListInput,
  env: Env,
): Promise<CatalogueListOutput> {
  // ...
}

Wenn ich catalogueListInput jemals um ein neues Pflichtfeld erweitere, scheitert jede Aufrufstelle, die es nicht liefert, beim Compile. Der TypeScript-Compiler erzwingt, was der Laufzeit-Validator schon erzwingt; beide hängen am selben Zod-Schema.

#Wie versioniert man MCP-Tool-Schemata?

Schemata entwickeln sich. Die Regel, die sich über mehrere Deployments gehalten hat:

Rückwärtskompatible Erweiterungen sind sicher. Ein neues optionales Feld mit Default-Wert bricht keinen bestehenden Agenten.

Rückwärtsinkompatible Änderungen verlangen einen neuen Tool-Namen. Wenn catalogue.list jemals ein neues Pflichtfeld oder ein umbenanntes existierendes Feld braucht, liefern Sie catalogue.list_v2 mindestens 90 Tage daneben. Markieren Sie catalogue.list in der Description als deprekiert; entfernen Sie es nach dem Deprekierungsfenster.

#Weitere MCP-Server-Anleitungen für WooCommerce

Dieser Artikel deckt typisierte Verträge ab. Für den Implementierungs-Durchstich siehe MCP-Server für WooCommerce aufbauen. Für die Auth-Strategie siehe MCP-Authentifizierungsmuster. Für die Protokoll-Entscheidung siehe MCP vs REST. Für den Migrationspfad aus einer bestehenden API siehe bestehende WordPress-API auf MCP migrieren. Der Pillar ist MCP-Server-Entwicklung.

Preisgestaltung individuell, weil der Umfang typisierter Verträge davon abhängt, wie breit die Agent-Intents sind, die Sie unterstützen wollen, und wie komplex die WooCommerce-Erweiterungen sind, die Sie kapseln.

Nächster Schritt

Machen Sie aus dem Artikel eine echte Umsetzung

Dieser Block stärkt die interne Verlinkung und führt Nutzer gezielt zum nächsten sinnvollen Schritt im Service- und Content-System.

Soll das Thema auf Ihrer Website umgesetzt werden?

Wenn Sie Headless WordPress, Frontend-Entkopplung oder eine Migration zu Astro planen, übernehme ich Architektur, WP-API und das Frontend.

Weitere WordPress-Dienste und Wissensbasis entdecken

Stärken Sie Ihr Unternehmen mit professionellem technischen Support in den Kernbereichen des WordPress-Ökosystems.

Häufig gestellte Fragen

Praktische Antworten zur Umsetzung des Themas.

Warum Zod statt purem JSON Schema?#
Das MCP-TypeScript-SDK akzeptiert Zod-Schemata direkt und konvertiert sie für die tools/list-Antwort in JSON Schema. Zod schreiben liefert Typinferenz via z.infer, Laufzeitvalidierung und eine einzige Quelle für SDK und typisierte Handler-Signatur.
Sollten Outputs wirklich validiert werden?#
Ja. Der Output-Parse fängt Handler-Bugs ab, die sonst fehlerhafte Daten an den Agenten leaken würden. Eine WooCommerce-Feldumbenennung in 9.x oder ein kaputter Plugin-Filter taucht als typisierter Validierungsfehler im Log auf, nicht als verwirrter Agent, der sich Details ausdenkt.
Wie streng soll die Input-Validierung sein?#
Streng genug, dass der Handler nie Müll bekommt. Nutzen Sie .strict(), um unbekannte Schlüssel abzulehnen, beschränken Sie String-Längen, deckeln Sie Zahlenbereiche und enumerieren Sie endliche Mengen. Dokumentieren Sie die Strenge in der Tool-Description, damit Agenten darüber räsonieren können.
Wie greifen Idempotenz-Schlüssel ins MCP-Protokoll?#
Der Idempotenz-Schlüssel ist nur ein optionales Inputfeld an mutierenden Tools. Das Protokoll weiß davon nichts. Der Handler prüft den Schlüssel gegen einen KV-Store, gibt das alte Ergebnis zurück, falls vorhanden, andernfalls führt er aus und cached.
Wo leben Fehlerantworten in MCP?#
MCP unterstützt JSON-RPC-Fehlerantworten für Protokoll-Fehler (invalid params, method not found) und Tool-Level-Fehler-Envelopes innerhalb erfolgreicher Tool-Antworten. Ich nutze das Tool-Level-Envelope für Domänenfehler (nicht auf Lager, Kunde nicht gefunden), damit der Agent darüber als Daten räsonieren kann.

Sie brauchen ein FAQ für Branche und Zielmarkt? Ich erstelle eine Version passend zu Ihren Business-Zielen.

Kontakt aufnehmen

Ähnliche Artikel