Schematy wejścia/wyjścia w Zod, klucze idempotencji, odpowiedzi błędów i typowane kontrakty narzędzi MCP opakowujących katalog WooCommerce.
PL

Pisanie typowanych narzędzi katalogu z Zod dla MCP

4.70 /5 - (7 głosów )
Ostatnio zweryfikowano: 1 maja 2026
6min czytania
Przewodnik
500+ projektów WP
Integracja AI

#Pisanie typowanych narzędzi katalogu z Zod dla MCP

Typowany kontrakt to różnica między agentem niezawodnie wołającym twój sklep a agentem halucynującym nazwy parametrów. Zod (zod.dev) jest biblioteką schematów, której używa oficjalne @modelcontextprotocol/sdk; daje walidację w runtime, generację JSON Schema dla tools/list i wnioskowanie typów TypeScript z jednego źródła. To wzorzec, którego używam dla każdego narzędzia MCP, jakie wdrażam.

Artykuł osadzony jest w filarze budowa serwera MCP.

#TL;DR

  • Pisz schematy wejścia i wyjścia w Zod; pozwól z.infer wyprowadzić sygnaturę handlera.
  • Używaj .strict(), by nieznane klucze padały od razu na granicy protokołu.
  • Lustruj słownik schema.org w wyjściach, jeśli istnieje odpowiadający typ.
  • Narzędzia mutujące przyjmują opcjonalny idempotency_key; memoizuj po kluczu w Workers KV.
  • Envelope błędu na poziomie narzędzia dla błędów domenowych; błędy JSON-RPC dla awarii protokołu.

#Nazewnictwo narzędzia

Nazwy narzędzi to publiczny kontrakt z każdym agentem, który kiedykolwiek zawoła twój serwer. Mają czytać się jak intencje typu czasownik-rzeczownik, nie jak endpointy REST:

  • Dobrze: catalogue.list, product.detail, order.intent, inventory.check, order.status.
  • Źle: wc_products_get, getProductById, searchCatalog.

Gdy agent został zbudowany pod catalogue.list, zmiana nazwy na catalog.list rozkłada każdego konsumenta po cichu, bo agent przestaje znajdować narzędzie. Traktuj nazwę jak zobowiązanie release’owe.

#Schematy wejścia: ciasne i jawne

Mój default dla każdego schematu wejścia:

import { z } from "zod";

export const catalogueListInput = z
  .object({
    query: z.string().min(2).max(200).optional()
      .describe("Wyszukiwanie pełnotekstowe po nazwie produktu i SKU"),
    category: z.string().optional()
      .describe("Slug kategorii w formie z WooCommerce"),
    in_stock: z.boolean().optional()
      .describe("Ogranicz do produktów aktualnie dostępnych"),
    price_min: z.number().nonnegative().optional()
      .describe("Dolne ograniczenie ceny regularnej, włącznie"),
    price_max: z.number().nonnegative().optional()
      .describe("Górne ograniczenie ceny regularnej, włącznie"),
    page: z.number().int().min(1).max(100).default(1)
      .describe("Numer strony, indeksowany od 1"),
    per_page: z.number().int().min(1).max(50).default(12)
      .describe("Wyniki na stronę, maks. 50"),
  })
  .strict()
  .refine(
    (v) => v.price_min === undefined || v.price_max === undefined || v.price_min <= v.price_max,
    { message: "price_min musi być mniejszy lub równy price_max" }
  );

Trzy rzeczy do podkreślenia:

.describe() wszędzie. SDK MCP propaguje opis do JSON Schema zwracanego przez tools/list. Agent to czyta. Pole bez opisu to pole, które agent musi zgadywać; pole z opisem to pole, które agent trafia poprawnie.

.strict() na obiekcie. Nieznane klucze padają w walidacji. Agent wysyłający { query: "buty", katgoria: "meskie" } (literówka) dostaje czytelny błąd walidacji wskazujący katgoria, zamiast udanego wywołania, które po cichu ignoruje przekręcony filtr.

.refine() dla reguł międzypolowych. Wszystko, co zależy od więcej niż jednego pola, idzie do .refine(). Ograniczenie zakresu cenowego to przykład kanoniczny; “tylko jedno z” to drugi.

#Schematy wyjścia: lustrzane do sklepu

Schemat wyjścia robi dwie rzeczy: dokumentuje kształt odpowiedzi dla agenta i waliduje wartość zwracaną przez handler, zanim opuści ona serwer.

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),
});

Dwa powody, by lustrować schema.org wprost:

Ten sam słownik między powierzchniami. Twój sklep emituje JSON-LD typu Product na stronie produktu. Twoje narzędzie MCP product.detail zwraca ten sam kształt. Agent zszywający kontekst z “strony, którą zaindeksowałem na /sklep/widget” i “tego, co powiedział mi twój serwer MCP” widzi jeden słownik, nie dwa.

Stabilne identyfikatory w availability. https://schema.org/InStock to URL; jest trwały, czytelny maszynowo i jednoznaczny. Wolny string typu "tak" lub "in stock" to problem tłumaczeniowy.

Handler woła parse na wyjściu:

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

Jeśli mapowanie WooCommerce zwróci uszkodzony Product, parse rzuca wyjątek i błąd jest łapany wyżej. Agent nigdy nie zobaczy uszkodzonych danych; deweloper zobaczy typowany błąd walidacji w logu.

#Klucze idempotencji dla narzędzi mutujących

Narzędzia odczytu są naturalnie idempotentne. Narzędzia mutujące potrzebują jawnego klucza. Wzorzec ustanowiony przez Stripe jest tym, którego się trzymam (dokumentacja idempotencji Stripe):

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("Opcjonalny UUID. Powtórzone wywołania z tym samym kluczem zwracają oryginalny szkic zamówienia."),
  })
  .strict();

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 godziny to default TTL, bo to dość, by burza retry agenta opadła, i mało, by KV pozostało małe.

#Envelope błędu

Dwa rodzaje błędów, dwa transporty:

Błędy protokołu. Method not found, invalid params, parse error. Te jadą jako odpowiedzi błędów JSON-RPC z kodami -32700 do -32603 (specyfikacja JSON-RPC). SDK MCP obsługuje je automatycznie, gdy walidacja wejścia w Zod padnie.

Błędy domenowe. Brak na stanie, klient nieznany, niewystarczający scope. Te jadą wewnątrz udanej odpowiedzi narzędzia, w strukturalnym polu error. Agent traktuje je jako dane, rozumuje o nich i decyduje co dalej.

Envelope, którego używam:

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,
  }),
]);

Discriminated union zmusza agenta do sprawdzenia status, zanim odczyta jakiekolwiek inne pole. “Udana odpowiedź z pustym szkicem” nigdy nie przechodzi typecheck’u; albo dostarczasz status: "ok" z danymi zamówienia, albo status: "error" z envelope’em.

#Wnioskowanie typów ze schematów

Schematy są jedynym źródłem prawdy. Typy wychodzą przez 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>;

Sygnatura handlera używa ich wprost:

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

Jeśli kiedykolwiek dorzucę nowe pole obowiązkowe do catalogueListInput, każde miejsce wywołania, które go nie dostarcza, padnie przy kompilacji. Kompilator TypeScript egzekwuje to, co już egzekwuje walidator runtime; oba siedzą na tym samym schemacie Zod.

#Wersjonowanie schematów

Schematy ewoluują. Reguła, która utrzymuje się przez kilka wdrożeń:

Zmiany kompatybilne wstecz są bezpieczne. Nowe pole opcjonalne z wartością domyślną nie psuje istniejących agentów.

Zmiany niekompatybilne wymagają nowej nazwy narzędzia. Jeśli catalogue.list kiedykolwiek potrzebuje nowego pola obowiązkowego lub przemianowania istniejącego, wystaw catalogue.list_v2 obok przez co najmniej 90 dni. Oznacz catalogue.list jako deprekowany w opisie; usuń po oknie deprekacji.

#Gdzie to siedzi w klastrze

Ten artykuł pokrywa typowane kontrakty. Po przebieg implementacji sięgnij do budowy serwera MCP dla WooCommerce. Po strategię auth do wzorców uwierzytelniania MCP. Po decyzję na poziomie protokołu do MCP vs REST. Po ścieżkę migracji z istniejącego API do migracji API WordPress do MCP. Filar to budowa serwera MCP.

Wycena indywidualna, bo zakres typowanych kontraktów zależy od szerokości intencji agenta, które chcesz wspierać, i od złożoności rozszerzeń WooCommerce, które opakowujesz.

Następny krok

Przekuj artykuł w realne wdrożenie

Pod tym wpisem dokładam linki, które domykają intencję użytkownika i prowadzą dalej w strukturze serwisu.

Chcesz wdrożyć ten temat na swojej stronie?

Jeśli zależy Ci na widoczności w Google i systemach AI, mogę przygotować architekturę treści, FAQ, schema i linkowanie pod GEO, AEO i SEO.

Powiązany klaster

Sprawdź inne usługi WordPress i bazę wiedzy

Wzmocnij swój biznes dzięki profesjonalnemu wsparciu technicznemu w kluczowych obszarach ekosystemu WordPress.

FAQ do artykułu

Często zadawane pytania

Najważniejsze odpowiedzi, które pomagają wdrożyć temat w praktyce.

SEO-ready GEO-ready AEO-ready 5 Q&A
Dlaczego Zod, a nie czysty JSON Schema?
SDK TypeScript MCP akceptuje schematy Zod wprost i konwertuje je do JSON Schema dla odpowiedzi tools/list. Pisanie Zod daje wnioskowanie typów przez z.infer, walidację w runtime i jedno źródło dla SDK i typowanej sygnatury handlera.
Czy wyjścia naprawdę warto walidować?
Tak. Parse na wyjściu łapie błędy handlera, które inaczej wyciekałyby do agenta jako zniekształcone dane. Zmiana nazwy pola w WooCommerce 9.x lub zepsuty filtr wtyczki pojawi się jako typowany błąd walidacji w logu, a nie jako zdezorientowany agent zmyślający szczegóły.
Jak ścisła powinna być walidacja wejścia?
Na tyle ścisła, by handler nigdy nie dostał śmieci. Użyj .strict(), by odrzucić nieznane klucze, ogranicz długości stringów, zwiąż zakresy liczbowe i wyliczaj zbiory skończone. Udokumentuj rygor w opisie narzędzia, by agenci mogli o tym rozumować.
Jak klucze idempotencji wpinają się w protokół MCP?
Klucz idempotencji jest tylko opcjonalnym polem wejścia w narzędziach mutujących. Protokół o nim nie wie. Handler sprawdza klucz w KV, zwraca poprzedni wynik, jeśli jest, w przeciwnym razie wykonuje i cache'uje.
Gdzie żyją odpowiedzi błędów w MCP?
MCP wspiera odpowiedzi błędów JSON-RPC dla awarii na poziomie protokołu (invalid params, method not found) i envelope'y błędów na poziomie narzędzia w odpowiedziach narzędzi udanych. Używam envelope'a narzędziowego dla błędów domenowych (brak na stanie, klient nieznany), by agent mógł rozumować o nich jak o danych.

Potrzebujesz FAQ dopasowanego do branży i rynku? Przygotujemy wersję pod Twoje cele biznesowe.

Porozmawiajmy

Polecane artykuły

Praktyczny przebieg budowy serwera Model Context Protocol przed WooCommerce. Definicje narzędzi, endpointy katalogu i zamówień, zgodność ze schema.org, walidacja Zod oraz wdrożenie na Cloudflare Workers, z którym agent AI potrafi rozmawiać.
wordpress

Budowa serwera MCP dla WooCommerce: przewodnik praktyka

Praktyczny przebieg budowy serwera Model Context Protocol przed WooCommerce. Definicje narzędzi, endpointy katalogu i zamówień, zgodność ze schema.org, walidacja Zod oraz wdrożenie na Cloudflare Workers, z którym agent AI potrafi rozmawiać.

Przewodnik decyzyjny do wyboru pomiędzy Model Context Protocol a REST API, gdy konsumentem jest agent AI. Typowana powierzchnia vs wnioskowanie kształtu JSON, akcje mutujące, uwierzytelnianie i wzorzec hybrydowy bijący oba.
wordpress

MCP vs REST: kiedy co wygrywa dla integracji agentów AI

Przewodnik decyzyjny do wyboru pomiędzy Model Context Protocol a REST API, gdy konsumentem jest agent AI. Typowana powierzchnia vs wnioskowanie kształtu JSON, akcje mutujące, uwierzytelnianie i wzorzec hybrydowy bijący oba.

Czterotygodniowy playbook migracji do postawienia serwera Model Context Protocol przed istniejącym REST API WordPressa. Audyt endpointów, scaffold MCP, parallel-run, cutover i obserwowalność, która czyni przejście bezpiecznym.
wordpress

Migracja istniejącego API WordPress do MCP: 4-tygodniowy playbook

Czterotygodniowy playbook migracji do postawienia serwera Model Context Protocol przed istniejącym REST API WordPressa. Audyt endpointów, scaffold MCP, parallel-run, cutover i obserwowalność, która czyni przejście bezpiecznym.