
# Prompts

> Reusable message templates, filled in by the client.

A prompt returns messages the client can drop into a conversation. Clients list them with `prompts/list` and expand them with `prompts/get`.

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

const reviewPrompt = definePrompt({
  name: "code-review",
  title: "Code review",
  description: "Review code for best practices",
  arguments: [
    { name: "code", description: "Code to review", required: true },
    { name: "language", description: "Programming language" },
  ],
  handler: async (args, event) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: `Review this ${args.language || ""} code:\n\n${args.code}`,
        },
      },
    ],
  }),
});
```

The playground's `greet` prompt is one of these — fill the arguments in and the panel shows the expanded messages exactly as `prompts/get` returns them:

::playground{item="prompts/greet" label="prompts/greet"}
::

## With and Without Arguments

The definition is a tagged union: declare `arguments` and your handler receives `(args, event)`; omit it and the handler takes `(event)` alone.

```ts
const helpPrompt = definePrompt({
  name: "help",
  description: "Show available commands",
  handler: async (event) => ({
    messages: [
      {
        role: "assistant",
        content: { type: "text", text: "Here are the available commands..." },
      },
    ],
  }),
});
```

Arguments declared `required: true` are enforced before the handler runs — a `prompts/get` missing one is rejected at the boundary, so `args.code` above is always present. So is the wire's own rule that argument values are strings: a non-string value is rejected before the handler sees it.

## Schema-Validated Arguments

`arguments` also accepts a [Standard JSON Schema](https://standardschema.dev/json-schema) implementor — Zod v4, ArkType, or Valibot wrapped in `toStandardJsonSchema()` — instead of the declarative list. The schema is both **advertised** (its object properties become the `arguments` array in `prompts/list`) and **enforced** before the handler runs, and the handler's `args` type is inferred from it:

```ts [zod]
import { z } from "zod";

const releasePrompt = definePrompt({
  name: "release",
  arguments: z.object({
    version: z.string().describe("Semver to release"),
    channel: z.enum(["stable", "beta"]).optional(),
  }),
  handler: async (args) => {
    // args: { version: string; channel?: "stable" | "beta" }
    return {
      messages: [
        {
          role: "user",
          content: { type: "text", text: `Cut ${args.version} on ${args.channel ?? "stable"}` },
        },
      ],
    };
  },
});
```

```ts [valibot]
import * as v from "valibot";
import { toStandardJsonSchema } from "@valibot/to-json-schema";

const releasePrompt = definePrompt({
  name: "release",
  arguments: toStandardJsonSchema(
    v.object({
      version: v.pipe(v.string(), v.description("Semver to release")),
      channel: v.optional(v.picklist(["stable", "beta"])),
    }),
  ),
  handler: async (args) => {
    // args: { version: string; channel?: "stable" | "beta" }
    return {
      messages: [
        {
          role: "user",
          content: { type: "text", text: `Cut ${args.version} on ${args.channel ?? "stable"}` },
        },
      ],
    };
  },
});
```

```ts [json schema]
const releasePrompt = definePrompt({
  name: "release",
  arguments: {
    type: "object",
    properties: {
      version: { type: "string", description: "Semver to release" },
      channel: { type: "string", enum: ["stable", "beta"] },
    },
    required: ["version"],
  },
  // `version` is present and a string; `channel` is whatever the client sent.
  handler: async (args) => ({
    messages: [
      {
        role: "user",
        content: { type: "text", text: `Cut ${args.version} on ${args.channel ?? "stable"}` },
      },
    ],
  }),
});
```

A plain JSON Schema object works too, as the third tab shows, but there is no validator to run, so it falls back to the same baseline the declarative list gets: required arguments must be present and values must be strings. Anything beyond that — formats, enums, ranges — is advertised but not enforced. Per-argument `complete` callbacks live on the declarative list, so a schema-declared prompt has no autocompletion to advertise.

A validation failure is a JSON-RPC error (`-32602`) on every protocol revision — unlike a tool, a prompt has no `isError` result to hand a correction back through. The message is static; the schema's own detail arrives in `error.data.validation`.

## Message Content

Messages carry the same content blocks as tool results, so a prompt can embed an image or a resource:

```ts
handler: async (args) => ({
  messages: [
    { role: "user", content: { type: "text", text: "What changed in this diff?" } },
    {
      role: "user",
      content: {
        type: "resource",
        resource: { uri: "app:///HEAD.diff", mimeType: "text/x-diff", text: await getDiff() },
      },
    },
  ],
});
```

## Autocompletion

Give an argument a `complete` callback and clients can autocomplete it as the user types:

```ts
const deployPrompt = definePrompt({
  name: "deploy",
  arguments: [
    {
      name: "environment",
      required: true,
      complete: async ({ argument }, event) => ({
        values: ["production", "staging", "development"].filter((e) =>
          e.startsWith(argument.value),
        ),
      }),
    },
  ],
  handler: async (args) => ({
    messages: [{ role: "user", content: { type: "text", text: `Deploy to ${args.environment}` } }],
  }),
});
```

:read-more{to="/guide/completions" title="Completions"}

## Icons

```ts
definePrompt({
  name: "summarize",
  icons: [{ src: "https://example.com/summarize.svg", mimeType: "image/svg+xml" }],
  handler: async (event) => ({ messages: [] }),
});
```

## Metadata

Every definition, result, and content block accepts a `_meta` object — the protocol's extension mechanism. Keys should carry a reverse-DNS prefix so independent extensions cannot collide:

```ts
definePrompt({
  name: "summarize",
  _meta: { "com.example/category": "writing" },
  handler: async () => ({
    _meta: { "com.example/tokens": 128 },
    messages: [{ role: "user", content: { type: "text", text: "Summarize this." } }],
  }),
});
```

Definition `_meta` is emitted by `prompts/list`; result `_meta` reaches the client as sent. On the modern era the server merges its own reserved `io.modelcontextprotocol/serverInfo` key in without overwriting anything you set.
