Skip to content

Schema generation (Zod)

The TypeScript SDK converts your Zod schemas into MCP-compliant JSON Schema using zod-to-json-schema. The wrapper lives in platools/core/schema.ts and exposes three helpers:

import { buildInputSchema, buildOutputSchema, buildSchemas, SchemaError } from "@platools/sdk";

buildSchemas

Used internally by the tool factory. Given a ToolOptions, it returns the inputSchema and outputSchema as JsonSchema objects:

const { inputSchema, outputSchema } = buildSchemas({
name: "list_tickets",
input: z.object({ customerId: z.string().uuid() }),
output: z.array(z.object({ id: z.string() })),
});

Internally:

  1. zodToJsonSchema(options.input, { target: "jsonSchema7" }) produces the input schema.
  2. zodToJsonSchema(options.output, ...) produces the output schema if output is set, null otherwise.
  3. Both are validated through the JsonSchema shape defined in types.ts so downstream consumers (doctor, transport) get a known-good shape.

SchemaError

Raised when Zod can’t be converted - typically happens if you pass something that isn’t a ZodTypeAny (a type-level mistake the TS compiler should catch, but runtime validation still guards it).

Descriptions

Zod’s .describe() maps directly to JSON Schema description, so you can document params inline:

export const createInvoice = platools.tool(
{
name: "create_invoice",
description: "Create a draft invoice for a customer",
input: z.object({
customerId: z.string().uuid().describe("Opaque UUID of the billed customer"),
amountCents: z.number().int().positive().describe("Invoice total in cents"),
currency: z
.enum(["usd", "eur", "gbp"])
.default("usd")
.describe("ISO-4217 currency code, lowercase"),
}),
output: z.object({ invoiceId: z.string() }),
auth: "admin",
roles: ["billing"],
},
async ({ customerId, amountCents, currency }) => {
return billingService.createInvoice({ customerId, amountCents, currency });
},
);

The generated input schema will surface those descriptions in every MCP client’s tool picker.

Supported Zod shapes

Everything zod-to-json-schema handles maps cleanly, including:

  • z.string(), z.number(), z.boolean(), z.null(), z.undefined()
  • z.enum(...), z.literal(...), z.nativeEnum(...)
  • z.object({ ... }), z.array(...), z.tuple(...)
  • z.union(...), z.discriminatedUnion(...), z.intersection(...)
  • z.record(...), z.map(...), z.set(...)
  • .optional(), .nullable(), .default(...)
  • .min(...), .max(...), .length(...), .regex(...), .email(), .url(), .uuid()

Custom refinements (.refine(...)) don’t round-trip through JSON Schema - they still run at handler-dispatch time, but they aren’t visible to the MCP client. Flag them explicitly in the tool description if a refinement rejects inputs the JSON Schema wouldn’t reject on its own.

Sharing schemas with UI code

Because the Zod schemas are the source of truth, you can re-use them in your frontend validators, backend API routes, and Platools tool registrations without drift. A common pattern:

// src/schemas/ticket.ts - shared
import { z } from "zod";
export const TicketStatus = z.enum(["open", "pending", "resolved", "closed"]);
export const Ticket = z.object({ /* ... */ });
// src/tools/list_tickets.ts
import { Ticket, TicketStatus } from "../schemas/ticket.js";
export const listTickets = platools.tool(/* uses the same shapes */);
// src/api/tickets.ts
import { Ticket } from "../schemas/ticket.js";
// validate API response bodies with the same schema

This is a big win over the Python SDK’s introspection-of-type-hints approach. There’s no separate “Pydantic model / OpenAPI schema / TypeScript type” split to keep in sync.

Next steps

  • Tool factory - the platools.tool() options and handler typing.
  • Client - WebSocket transport with heartbeat and backoff.
  • Python Schemas - the equivalent Pydantic-based approach.