Ferramentas de catálogo tipadas com Zod para MCP

Ferramentas de catálogo tipadas com Zod para MCP

Última verificação: 22 de setembro de 2026
7 min de leitura
Guia
500+ projetos WP
Integração IA

#Ferramentas de catálogo tipadas com Zod para MCP

Um contrato tipado é a diferença entre um agente que chama a sua loja de forma fiável e um agente que alucina nomes de parâmetros. O Zod (zod.dev) é a biblioteca de esquemas usada pelo @modelcontextprotocol/sdk oficial: dá validação em tempo de execução, geração de JSON Schema para tools/list e inferência de tipos TypeScript a partir de uma única fonte. É o padrão que uso em todas as ferramentas MCP que entrego.

Este artigo está ligado ao pilar desenvolvimento de servidores MCP.

#TL;DR

  • Escreva os esquemas de entrada e saída em Zod e deixe que z.infer derive a assinatura do handler.
  • Use .strict() para que as chaves desconhecidas falhem logo na fronteira do protocolo.
  • Espelhe o vocabulário schema.org nas saídas sempre que exista um tipo correspondente.
  • As ferramentas que alteram dados aceitam uma idempotency_key opcional; memorize por chave em Workers KV.
  • Envelope de erro ao nível da ferramenta para erros de domínio; erros JSON-RPC para falhas de protocolo.

#Como dar nome às ferramentas MCP

Os nomes das ferramentas são o contrato público com cada agente que alguma vez chame o seu servidor. Devem ler-se como intenções do tipo verbo e objeto, e não como endpoints REST:

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

Depois de um agente ter sido construído com base em catalogue.list, mudar o nome para catalog.list parte todos os consumidores em silêncio, porque o agente deixa de encontrar a ferramenta. Trate o nome como um compromisso de release.

#Esquemas de entrada Zod rigorosos para ferramentas MCP

A configuração por omissão que escrevo para cada esquema de entrada:

import { z } from "zod";

export const catalogueListInput = z
  .object({
    query: z.string().min(2).max(200).optional()
      .describe("Pesquisa de texto livre no nome do produto e no SKU"),
    category: z.string().optional()
      .describe("Slug da categoria tal como aparece no WooCommerce"),
    in_stock: z.boolean().optional()
      .describe("Limitar a produtos atualmente em stock"),
    price_min: z.number().nonnegative().optional()
      .describe("Limite inferior do preço normal, inclusive"),
    price_max: z.number().nonnegative().optional()
      .describe("Limite superior do preço normal, inclusive"),
    page: z.number().int().min(1).max(100).default(1)
      .describe("Número da página, indexado a partir de 1"),
    per_page: z.number().int().min(1).max(50).default(12)
      .describe("Resultados por página, máx. 50"),
  })
  .strict()
  .refine(
    (v) => v.price_min === undefined || v.price_max === undefined || v.price_min <= v.price_max,
    { message: "price_min tem de ser menor ou igual a price_max" }
  );

Três pontos a destacar:

.describe() em todo o lado. O SDK do MCP propaga a descrição para o JSON Schema devolvido por tools/list. O agente lê-a. Um campo sem descrição é um campo que o agente tem de adivinhar; um campo com descrição é um campo que o agente acerta.

.strict() no objeto. As chaves desconhecidas falham na validação. Um agente que envie { query: "botas", categria: "homem" } (gralha) recebe um erro de validação claro a apontar para categria, em vez de uma chamada bem-sucedida que ignora em silêncio o filtro mal escrito.

.refine() para regras entre campos. Tudo o que depende de mais de um campo vai para .refine(). A restrição do intervalo de preços é o exemplo canónico; “apenas um de” é o outro.

#Esquema de saída Zod alinhado com schema.org Product

O esquema de saída faz duas coisas: documenta a forma da resposta para o agente e valida o valor devolvido pelo handler antes de este sair do servidor.

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

Duas razões para espelhar o schema.org diretamente:

O mesmo vocabulário em todas as superfícies. A sua loja emite JSON-LD do tipo Product na página de produto. A sua ferramenta MCP product.detail devolve a mesma forma. Um agente que junta contexto de “a página que indexei em /loja/widget” e de “o que o seu servidor MCP me disse” vê um só vocabulário, e não dois.

Identificadores estáveis em availability. https://schema.org/InStock é um URL: é duradouro, legível por máquinas e inequívoco. Uma string livre como "sim" ou "in stock" é um problema de tradução.

O handler chama parse sobre a saída:

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

Se o mapeamento do WooCommerce devolver um Product corrompido, o parse lança uma exceção que é apanhada mais acima. O agente nunca vê dados corrompidos; o programador vê um erro de validação tipado no registo.

#Chaves de idempotência em ferramentas MCP que escrevem dados

As ferramentas de leitura são naturalmente idempotentes. As ferramentas que alteram dados precisam de uma chave explícita. O padrão estabelecido pela Stripe é o que sigo (documentação de idempotência da 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("UUID opcional. Chamadas repetidas com a mesma chave devolvem o rascunho de encomenda original."),
  })
  .strict();

O 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 horas é o TTL por omissão porque chega para que uma tempestade de novas tentativas do agente acalme, e é pouco o suficiente para que o KV se mantenha pequeno.

#Erros de protocolo e erros de domínio no MCP

Dois tipos de erro, dois transportes:

Erros de protocolo. Method not found, invalid params, parse error. Estes seguem como respostas de erro JSON-RPC com os códigos -32700 a -32603 (especificação JSON-RPC). O SDK do MCP trata-os automaticamente quando a validação da entrada em Zod falha.

Erros de domínio. Sem stock, cliente desconhecido, scope insuficiente. Estes seguem dentro de uma resposta bem-sucedida da ferramenta, num campo error estruturado. O agente trata-os como dados, raciocina sobre eles e decide o passo seguinte.

O envelope que uso:

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

A discriminated union obriga o agente a verificar status antes de ler qualquer outro campo. Uma “resposta bem-sucedida com rascunho vazio” nunca passa a verificação de tipos: ou entrega status: "ok" com os dados da encomenda, ou status: "error" com o envelope.

#Tipos TypeScript a partir de esquemas Zod com z.infer

Os esquemas são a única fonte de verdade. Os tipos saem através de 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>;

A assinatura do handler usa-os diretamente:

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

Se alguma vez eu acrescentar um novo campo obrigatório a catalogueListInput, todos os pontos de chamada que não o forneçam falham na compilação. O compilador de TypeScript impõe o que o validador em tempo de execução já impõe; ambos assentam no mesmo esquema Zod.

#Como versionar os esquemas das ferramentas MCP

Os esquemas evoluem. A regra que se tem mantido ao longo de várias entregas:

As alterações retrocompatíveis são seguras. Um novo campo opcional com valor por omissão não parte os agentes existentes.

As alterações incompatíveis exigem um novo nome de ferramenta. Se catalogue.list precisar algum dia de um novo campo obrigatório ou de renomear um campo existente, publique catalogue.list_v2 em paralelo durante pelo menos 90 dias. Marque catalogue.list como obsoleta na descrição e remova-a depois da janela de descontinuação.

#Mais sobre servidores MCP para WooCommerce

Este artigo trata dos contratos tipados. A implementação em si, a estratégia de autenticação, a escolha entre MCP e REST e a migração a partir de uma API WordPress existente são temas à parte. A página do serviço é desenvolvimento de servidores MCP.

O preço é definido caso a caso, porque o âmbito dos contratos tipados depende da amplitude das intenções de agente que pretende suportar e da complexidade das extensões WooCommerce que está a envolver.

Próximo passo

Transforme o artigo numa implementação real

Este bloco reforça a ligação interna e conduz o leitor para o passo seguinte mais útil dentro da arquitetura do site.

Quer implementar isto no seu site?

Se está a planear headless WordPress, desacoplamento de frontend ou migração para Astro, posso desenhar e implementar a arquitetura completa.

Cluster relacionado

Explorar outros serviços WordPress e base de conhecimento

Reforce o seu negócio com suporte técnico profissional em áreas-chave do ecossistema WordPress.

FAQ do artigo

Perguntas frequentes

Respostas práticas para aplicar o tema na execução real.

SEO-readyGEO-readyAEO-ready5 Q&A
Porquê Zod e não JSON Schema puro?#
O SDK de TypeScript para MCP aceita esquemas Zod diretamente e converte-os em JSON Schema para a resposta de tools/list. Escrever em Zod dá inferência de tipos através de z.infer, validação em tempo de execução e uma única fonte para o SDK e para a assinatura tipada do handler.
Vale mesmo a pena validar as saídas?#
Sim. O parse na saída apanha erros do handler que, de outro modo, chegariam ao agente como dados malformados. Um campo renomeado no WooCommerce 9.x ou um filtro de plugin avariado aparece como um erro de validação tipado no registo, e não como um agente confuso a inventar detalhes.
Quão rigorosa deve ser a validação da entrada?#
Rigorosa o suficiente para que o handler nunca receba lixo. Use .strict() para rejeitar chaves desconhecidas, limite o comprimento das strings, restrinja os intervalos numéricos e enumere os conjuntos finitos. Documente esse rigor na descrição da ferramenta, para que os agentes possam raciocinar sobre ele.
Como encaixam as chaves de idempotência no protocolo MCP?#
A chave de idempotência é apenas um campo de entrada opcional nas ferramentas que alteram dados. O protocolo não sabe da sua existência. O handler procura a chave em KV, devolve o resultado anterior se existir e, caso contrário, executa e guarda o resultado em cache.
Onde ficam as respostas de erro no MCP?#
O MCP suporta respostas de erro JSON-RPC para falhas ao nível do protocolo (invalid params, method not found) e envelopes de erro ao nível da ferramenta dentro de respostas bem-sucedidas. Uso o envelope da ferramenta para erros de domínio (sem stock, cliente desconhecido), para que o agente possa raciocinar sobre eles como dados.

Precisa de FAQ adaptado ao setor e mercado? Criamos uma versão alinhada com os seus objetivos de negócio.

Fale connosco

Artigos Relacionados