Typede katalogverktøy med Zod for MCP

Typede katalogverktøy med Zod for MCP

Sist verifisert: 22. september 2026
6 min lesetid
Guide
500+ WP-prosjekter
AI-integrasjon

#Typede katalogverktøy med Zod for MCP

En typet kontrakt er forskjellen på en agent som kaller butikken din pålitelig og en agent som hallusinerer parameternavn. Zod (zod.dev) er skjemabiblioteket som den offisielle @modelcontextprotocol/sdk bruker. Det gir validering under kjøring, generering av JSON Schema for tools/list og TypeScript-typeinferens fra én kilde. Dette er mønsteret jeg bruker for hvert MCP-verktøy jeg leverer.

Artikkelen hører til søylesiden utvikling av MCP-servere.

#TL;DR

  • Skriv inndata- og utdataskjemaer i Zod, og la z.infer utlede signaturen til handleren.
  • Bruk .strict() slik at ukjente nøkler feiler med en gang ved protokollgrensen.
  • Speil schema.org-vokabularet i utdata når det finnes en tilsvarende type.
  • Verktøy som endrer data tar imot en valgfri idempotency_key; memoiser per nøkkel i Workers KV.
  • Feilkonvolutt på verktøynivå for domenefeil; JSON-RPC-feil for protokollfeil.

#Slik navngir du MCP-verktøy

Verktøynavn er den offentlige kontrakten med hver agent som noen gang kaller serveren din. De skal leses som intensjoner i formen verb og objekt, ikke som REST-endepunkter:

  • Bra: catalogue.list, product.detail, order.intent, inventory.check, order.status.
  • Dårlig: wc_products_get, getProductById, searchCatalog.

Når en agent er bygget mot catalogue.list, vil en omdøping til catalog.list stille ødelegge for alle konsumenter, fordi agenten ikke lenger finner verktøyet. Behandle navnet som en release-forpliktelse.

#Strenge Zod-inndataskjemaer for MCP-verktøy

Standardoppsettet mitt for hvert inndataskjema:

import { z } from "zod";

export const catalogueListInput = z
  .object({
    query: z.string().min(2).max(200).optional()
      .describe("Fritekstsøk i produktnavn og SKU"),
    category: z.string().optional()
      .describe("Kategori-slug slik den står i WooCommerce"),
    in_stock: z.boolean().optional()
      .describe("Begrens til produkter som er på lager nå"),
    price_min: z.number().nonnegative().optional()
      .describe("Nedre grense for ordinær pris, inkludert"),
    price_max: z.number().nonnegative().optional()
      .describe("Øvre grense for ordinær pris, inkludert"),
    page: z.number().int().min(1).max(100).default(1)
      .describe("Sidenummer, indeksert fra 1"),
    per_page: z.number().int().min(1).max(50).default(12)
      .describe("Resultater per side, maks 50"),
  })
  .strict()
  .refine(
    (v) => v.price_min === undefined || v.price_max === undefined || v.price_min <= v.price_max,
    { message: "price_min må være mindre enn eller lik price_max" }
  );

Tre ting verdt å merke seg:

.describe() overalt. MCP-SDK-et sender beskrivelsen videre til JSON Schema som returneres av tools/list. Agenten leser den. Et felt uten beskrivelse er et felt agenten må gjette seg til; et felt med beskrivelse er et felt agenten treffer riktig.

.strict() på objektet. Ukjente nøkler feiler i valideringen. En agent som sender { query: "støvler", katgori: "herre" } (skrivefeil) får en tydelig valideringsfeil som peker på katgori, i stedet for et vellykket kall som i stillhet ignorerer det feilstavede filteret.

.refine() for regler på tvers av felt. Alt som avhenger av mer enn ett felt hører hjemme i .refine(). Begrensningen på prisintervall er standardeksemplet; “bare ett av” er det andre.

#Zod-utdataskjema i tråd med schema.org Product

Utdataskjemaet gjør to ting: det dokumenterer svarets form for agenten, og det validerer verdien handleren returnerer før den forlater serveren.

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

To grunner til å speile schema.org direkte:

Samme vokabular på tvers av flater. Butikken din sender ut JSON-LD av typen Product på produktsiden. MCP-verktøyet ditt product.detail returnerer den samme formen. En agent som setter sammen kontekst fra “siden jeg indekserte på /butikk/widget” og “det MCP-serveren din fortalte meg” ser ett vokabular, ikke to.

Stabile identifikatorer i availability. https://schema.org/InStock er en URL; den er varig, maskinlesbar og entydig. En fri streng som "ja" eller "in stock" er et oversettelsesproblem.

Handleren kaller parse på utdata:

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

Hvis WooCommerce-mappingen returnerer et ødelagt Product, kaster parse et unntak som fanges lenger opp. Agenten ser aldri ødelagte data; utvikleren ser en typet valideringsfeil i loggen.

#Idempotensnøkler i MCP-verktøy som skriver data

Leseverktøy er naturlig idempotente. Verktøy som endrer data trenger en eksplisitt nøkkel. Mønsteret Stripe etablerte er det jeg følger (Stripes dokumentasjon om idempotens):

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("Valgfri UUID. Gjentatte kall med samme nøkkel returnerer det opprinnelige ordreutkastet."),
  })
  .strict();

Handleren:

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 timer er standard-TTL fordi det er nok til at en storm av nye forsøk fra agenten legger seg, og lite nok til at KV forblir lite.

#Protokollfeil og domenefeil i MCP

To typer feil, to transportveier:

Protokollfeil. Method not found, invalid params, parse error. Disse går som JSON-RPC-feilsvar med kodene -32700 til -32603 (JSON-RPC-spesifikasjonen). MCP-SDK-et håndterer dem automatisk når Zod-valideringen av inndata feiler.

Domenefeil. Ikke på lager, ukjent kunde, utilstrekkelig scope. Disse går inne i et vellykket verktøysvar, i et strukturert error-felt. Agenten behandler dem som data, resonnerer om dem og bestemmer hva som skjer videre.

Konvolutten jeg bruker:

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

En discriminated union tvinger agenten til å sjekke status før den leser noe annet felt. “Vellykket svar med tomt utkast” kommer aldri gjennom typesjekken; enten leverer du status: "ok" med ordredata, eller status: "error" med konvolutten.

#TypeScript-typer fra Zod-skjemaer med z.infer

Skjemaene er den eneste sannhetskilden. Typene kommer ut 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>;

Signaturen til handleren bruker dem direkte:

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

Hvis jeg noen gang legger til et nytt obligatorisk felt i catalogueListInput, feiler hvert kallsted som ikke leverer det ved kompilering. TypeScript-kompilatoren håndhever det valideringen under kjøring allerede håndhever; begge bygger på det samme Zod-skjemaet.

#Slik versjonerer du skjemaer for MCP-verktøy

Skjemaer utvikler seg. Regelen som har holdt gjennom flere leveranser:

Bakoverkompatible endringer er trygge. Et nytt valgfritt felt med standardverdi ødelegger ikke eksisterende agenter.

Endringer som bryter kompatibiliteten krever et nytt verktøynavn. Hvis catalogue.list noen gang trenger et nytt obligatorisk felt eller et omdøpt eksisterende felt, publiser catalogue.list_v2 ved siden av i minst 90 dager. Merk catalogue.list som utfaset i beskrivelsen, og fjern det etter utfasingsvinduet.

#Mer om MCP-servere for WooCommerce

Denne artikkelen handler om typede kontrakter. Selve implementeringen, autentiseringsstrategien, valget mellom MCP og REST og migreringen fra et eksisterende WordPress-API er egne temaer. Tjenestesiden er utvikling av MCP-servere.

Prisen settes individuelt, fordi omfanget av typede kontrakter avhenger av hvor bredt spekter av agentintensjoner du vil støtte, og av hvor komplekse WooCommerce-utvidelsene du pakker inn er.

Neste steg

Gjør artikkelen om til faktisk implementering

Denne blokken styrker intern lenking og sender leseren videre til de mest relevante tjenestene og innholdet.

Vil du få dette implementert på nettstedet ditt?

Hvis du planlegger headless WordPress, frikoblet frontend eller migrering til Astro, kan jeg bygge arkitektur, API og frontend.

Relevant klynge

Utforsk andre WordPress-tjenester og kunnskapsbase

Styrk virksomheten din med profesjonell teknisk støtte innen kjerneområdene i WordPress-økosystemet.

Artikkel-FAQ

Ofte stilte spørsmål

Praktiske svar for å bruke temaet i faktisk arbeid.

SEO-readyGEO-readyAEO-ready5 Q&A
Hvorfor Zod og ikke ren JSON Schema?#
TypeScript-SDK-et for MCP tar imot Zod-skjemaer direkte og konverterer dem til JSON Schema for svaret på tools/list. Med Zod får du typeinferens via z.infer, validering under kjøring og én kilde for både SDK-et og den typede signaturen til handleren.
Er det virkelig verdt å validere utdata?#
Ja. Parse på utdata fanger feil i handleren som ellers ville lekket til agenten som misformede data. Et omdøpt felt i WooCommerce 9.x eller et ødelagt filter i en utvidelse dukker opp som en typet valideringsfeil i loggen, ikke som en forvirret agent som dikter opp detaljer.
Hvor streng bør inndatavalideringen være?#
Streng nok til at handleren aldri får søppel. Bruk .strict() for å avvise ukjente nøkler, begrens lengden på strenger, sett grenser for tallområder og list opp endelige mengder som enum. Dokumenter strengheten i beskrivelsen av verktøyet, slik at agenter kan resonnere om den.
Hvordan passer idempotensnøkler inn i MCP-protokollen?#
Idempotensnøkkelen er bare et valgfritt inndatafelt på verktøy som endrer data. Protokollen vet ikke om den. Handleren slår opp nøkkelen i KV, returnerer forrige resultat hvis det finnes, og ellers kjører den og cacher resultatet.
Hvor hører feilsvar hjemme i MCP?#
MCP støtter JSON-RPC-feilsvar for feil på protokollnivå (invalid params, method not found) og feilkonvolutter på verktøynivå inne i vellykkede verktøysvar. Jeg bruker verktøykonvolutten for domenefeil (ikke på lager, ukjent kunde), slik at agenten kan resonnere om dem som data.

Trenger du FAQ tilpasset bransje og marked? Vi lager en versjon som støtter dine forretningsmål.

Ta kontakt

Relaterte artikler