h3-mcp logoh3-mcp

Zod Schemas

Typed, validated tool inputs with Standard Schema.

Pass a Standard JSON Schema implementor straight through as inputSchema and you get three things at once: JSON Schema for clients, a TypeScript type for your handler, and runtime validation at the boundary.

import { z } from "zod";
import { defineMcpTool } from "h3-mcp";

export const createIssue = defineMcpTool({
  name: "create-issue",
  description: "Open an issue in the tracker",
  inputSchema: z.object({
    title: z.string().min(1).max(200).describe("One-line summary"),
    body: z.string().optional(),
    labels: z.array(z.enum(["bug", "feature", "docs"])).default([]),
    priority: z.number().int().min(1).max(5).default(3),
  }),
  handler: async (args) => {
    // args: { title: string; body?: string; labels: (...)[]; priority: number }
    const issue = await tracker.create(args);
    return {
      content: [{ type: "text", text: `Created #${issue.number}` }],
      structuredContent: issue,
    };
  },
});

Minimum versions: Zod v4.2+, Valibot v1.2+, ArkType v2.1.28+ — earlier releases do not implement Standard JSON Schema.

#Descriptions Are the Interface

The schema is what the model reads to decide how to call your tool. .describe() is not decoration:

inputSchema: z.object({
  query: z.string().describe("Full-text search query. Supports quotes for exact phrases."),
  since: z.string().datetime().describe("ISO 8601 timestamp. Only results after this time."),
  limit: z
    .number()
    .int()
    .max(50)
    .default(10)
    .describe("Max results. Keep small; results are verbose."),
});

#Output Schemas

outputSchema works the same way and pairs with structuredContent:

defineMcpTool({
  name: "weather",
  inputSchema: z.object({ city: z.string() }),
  outputSchema: z.object({
    tempC: z.number(),
    conditions: z.enum(["clear", "cloudy", "rain", "snow"]),
  }),
  handler: async ({ city }) => {
    const data = await getWeather(city);
    return {
      content: [{ type: "text", text: `${data.conditions}, ${data.tempC}°C` }],
      structuredContent: data,
    };
  },
});

Always return content as well — a client that ignores structured output still needs something to show.

#Explicit Conversion

If you would rather control the emitted JSON Schema, convert it yourself:

const schema = z.object({ city: z.string() });

defineMcpTool({
  name: "weather",
  inputSchema: z.toJSONSchema(schema),
  handler: async (args) => {
    const { city } = schema.parse(args); // now validate explicitly
    return { content: [{ type: "text", text: city }] };
  },
});

Note

Converting up front means you also opt out of automatic validation — plain JSON Schema is advertised, not enforced. Parse inside the handler, as above. For Zod v3, use zod-to-json-schema.

#Validation Failures

Nothing invalid reaches the handler. On ≥2025-11-25 the client receives a tool result with isError: true (so the model can fix its call); on ≤2025-06-18 it receives a JSON-RPC error.

Read more in Input validation.
h3-mcp logo

h3-mcp  MCP servers, built on H3.