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.
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:
#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.
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 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:
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"}` },
},
],
};
},
});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:
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:
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}` } }],
}),
});#Icons
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:
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.