
# Zod Schemas

> Typed, validated tool inputs with Standard Schema.

Pass a [Standard JSON Schema](https://standardschema.dev/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.

::code-group

```ts [zod]
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,
    };
  },
});
```

```ts [valibot]
import * as v from "valibot";
import { defineMcpTool } from "h3-mcp";

export const createIssue = defineMcpTool({
  name: "create-issue",
  inputSchema: v.object({
    title: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
    body: v.optional(v.string()),
  }),
  handler: async (args) => ({ content: [{ type: "text", text: args.title }] }),
});
```

```ts [arktype]
import { type } from "arktype";
import { defineMcpTool } from "h3-mcp";

export const createIssue = defineMcpTool({
  name: "create-issue",
  inputSchema: type({ title: "string", "body?": "string" }),
  handler: async (args) => ({ content: [{ type: "text", text: args.title }] }),
});
```

::

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:

```ts
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`:

```ts
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:

```ts
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`](https://github.com/StefanTerdell/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{to="/security/validation" title="Input validation"}
