
# Tools

> Functions a model can call.

A tool is a name, an optional input schema, and a handler. Clients discover them with `tools/list` and invoke them with `tools/call`, on both eras.

```ts
import { defineMcpTool } from "h3-mcp";

const searchTool = defineMcpTool({
  name: "search",
  description: "Search documents by query",
  inputSchema: {
    type: "object",
    properties: {
      query: { type: "string" },
      limit: { type: "number" },
    },
    required: ["query"],
  },
  handler: async (args, event) => ({
    content: [{ type: "text", text: `Results for: ${args.query}` }],
  }),
});
```

## Handler Signature

The signature depends on whether the tool takes input:

```ts
// With `inputSchema` — (args, event)
defineMcpTool({
  name: "echo",
  inputSchema: { type: "object", properties: { message: { type: "string" } } },
  handler: async (args, event) => ({ content: [{ type: "text", text: args.message as string }] }),
});

// Without `inputSchema` — (event)
defineMcpTool({
  name: "status",
  description: "Get server status",
  handler: async (event) => ({ content: [{ type: "text", text: "OK" }] }),
});
```

## Standard Schema

[Standard JSON Schema](https://standardschema.dev/json-schema) implementors — Zod v4.2+, Valibot v1.2+, ArkType v2.1.28+ — can be passed straight through as `inputSchema`. They are resolved to plain JSON Schema when `tools/list` serializes them, **and** validated at runtime before your handler runs, so `args` is both typed and checked:

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

const createUserTool = defineMcpTool({
  name: "create-user",
  description: "Create a new user account",
  inputSchema: z.object({
    name: z.string().describe("Full name of the user"),
    email: z.string().email(),
    role: z.enum(["admin", "member"]).optional(),
  }),
  handler: async (args) => {
    // args: { name: string; email: string; role?: "admin" | "member" }
    return { content: [{ type: "text", text: `Created user ${args.name}` }] };
  },
});
```

Invalid arguments never reach the handler. How the failure is reported is version-aware, per [SEP-1303](https://modelcontextprotocol.io/specification/2026-07-28): clients on `≤2025-06-18` get a JSON-RPC error, clients on `≥2025-11-25` get a tool result with `isError: true` so the model can correct itself.

> [!NOTE]
> Plain JSON Schema `inputSchema` is advertised to clients but **not** validated at runtime — there is no validator to call. Use a Standard Schema implementor if you want server-side enforcement, or validate inside the handler.

You can also pre-convert with `z.toJSONSchema(schema)` for explicit control. For Zod v3, use [`zod-to-json-schema`](https://github.com/StefanTerdell/zod-to-json-schema).

:read-more{to="/security/validation" title="Input validation"}

## Results

A tool returns `content` — an array of content blocks:

```ts
// Text
return { content: [{ type: "text", text: "Hello" }] };

// Image or audio (base64)
return { content: [{ type: "image", data: "iVBOR...", mimeType: "image/png" }] };

// Embedded resource
return {
  content: [
    {
      type: "resource",
      resource: { uri: "app:///report.md", mimeType: "text/markdown", text: "# Report" },
    },
  ],
};
```

### Structured Output

Declare an `outputSchema` and return `structuredContent` alongside the human-readable blocks. Since `2026-07-28`, `structuredContent` may be any JSON value, not only an object:

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

### Errors

A tool that fails in a way the _model_ should see returns `isError: true` — this is a successful JSON-RPC response carrying a failed tool call:

```ts
handler: async (args) => {
  if (!args.data) {
    return {
      isError: true,
      content: [{ type: "text", text: "Validation failed: data is empty" }],
    };
  }
  return { content: [{ type: "text", text: "Valid" }] };
};
```

Throwing, by contrast, is a _protocol_ error and its message is not shown to the client on the modern era.

:read-more{to="/guide/basics/errors" title="Errors"}

## Annotations

Annotations are behavior hints clients use to decide whether to ask for confirmation:

```ts
const deleteTool = defineMcpTool({
  name: "delete-item",
  description: "Delete an item by ID",
  annotations: {
    title: "Delete item",
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: false,
  },
  inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
  handler: async (args) => ({ content: [{ type: "text", text: `Deleted ${args.id}` }] }),
});
```

They are hints, not enforcement — never rely on a client honoring them.

## Icons and Task Support

```ts
defineMcpTool({
  name: "render",
  icons: [{ src: "https://example.com/render.svg", mimeType: "image/svg+xml", theme: "dark" }],
  execution: { taskSupport: "optional" }, // "required" | "optional" | "forbidden"
  handler: async () => ({ content: [{ type: "text", text: "..." }] }),
});
```

## Long-Running Tools

Cooperate with cancellation via `event.context.mcp.signal`, and report progress with `event.context.mcp.progress()`:

```ts
const importTool = defineMcpTool({
  name: "import",
  handler: async (event) => {
    const mcp = event.context.mcp!;
    for (let i = 1; i <= 10; i++) {
      if (mcp.signal?.aborted) {
        return { isError: true, content: [{ type: "text", text: "Cancelled" }] };
      }
      mcp.progress?.(i, 10, `row ${i}`);
      await importRow(i);
    }
    return { content: [{ type: "text", text: "Imported" }] };
  },
});
```

:read-more{to="/guide/modern/streaming" title="Request-scoped streaming"}

## Asking the Client for Input

A tool that needs the user to answer something returns an interim `input_required` result instead of calling the client itself:

:read-more{to="/guide/modern/mrtr" title="Multi round-trip requests"}

## Pagination

`tools/list` is paginated with an opaque cursor automatically — you never handle `cursor` yourself. Clients that ignore `nextCursor` simply see the first page.
