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.