# @Prompt

> Declare a reusable message template that users pick in their client and fill in with arguments.

Source: https://frontmcp.dev/reference/sdk/prompt

`@Prompt` declares an MCP [prompt](https://frontmcp.dev/learn/reusable-prompts): a message template that a user picks in their client, often from a slash menu, and fills in with arguments. FrontMCP runs your `execute()` with those arguments and returns the messages it builds, which the client then puts in front of the model.

```ts
@Prompt(options)
class MyPrompt extends PromptContext {
  async execute(args): Promise<GetPromptResult> { /* ... */ }
}
```

---

## Reference

### `@Prompt(options)`

Apply `@Prompt` to a class that extends `PromptContext`, and list the class in an app's `prompts` array.

```ts triage-ticket.prompt.ts
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({
  name: "triage_ticket",
  title: "Triage a ticket",
  description: "Suggest a priority and a next step for a support ticket",
  arguments: [
    { name: "id", description: "Ticket id, like T-1", required: true },
    { name: "tone", description: "friendly or formal" },
  ],
})
class TriageTicket extends PromptContext {
  async execute(args: Record<string, string>): Promise<GetPromptResult> {
    return {
      messages: [
        { role: "user", content: { type: "text", text: `Read ticket ${args.id}, then suggest a priority and a next step.` } },
      ],
    };
  }
}
```

[See more examples below.](#usage)

#### Options

Required:

| Option | Type | Description |
| --- | --- | --- |
| `name` | `string` | What clients list and ask for with `prompts/get`. Unique within the server. |

Optional:

| Option | Type | Description |
| --- | --- | --- |
| `arguments` | `PromptArgument[]` | The values the user fills in. Leave it out for a prompt without arguments. |
| `title` | `string` | A readable name for menus, like `"Triage a ticket"`. Clients fall back to `name` without it. |
| `description` | `string` | What the prompt is for. Also becomes the result's `description` when `execute()` doesn't return one. |
| `icons` | `Icon[]` | Icons clients can show next to the prompt. Each has a `src`, and optionally `mimeType`, `sizes` and `theme` (`"light"` or `"dark"`). |
| `availableWhen` | `EntryAvailability` | Only offer the prompt in matching environments. Takes arrays of `os`, `platform`, `runtime`, `deployment`, `provider`, `target`, `surface` and `env` values. Elsewhere the prompt isn't listed, and `prompts/get` fails with [`-32003`](#prompt--is-not-available-in-the-current-environment). See [Environment awareness](https://frontmcp.dev/reference/server/environment). |

Each entry in `arguments`:

| Field | Type | Description |
| --- | --- | --- |
| `name` | `string` | The key the value arrives under in `args`. |
| `description` | `string` | Shown to the user filling it in. |
| `required` | `boolean` | Defaults to `false`. When `true`, FrontMCP rejects a `prompts/get` that leaves it out, and `execute()` doesn't run. |

#### `execute(args)`

FrontMCP calls `execute()` when a client sends `prompts/get`, once every required argument is present.

- `args`: the arguments the client sent, as strings. They're also available as `this.args`.
- **Returns** the prompt's messages. FrontMCP turns what `execute()` returns into the `prompts/get` result:

| `execute()` returns | The client receives |
| --- | --- |
| `{ description?, messages }` | Those messages. Each has a `role`, `"user"` or `"assistant"`, and one `content` block of type `text`, `image`, `audio`, `resource` (an embedded resource) or `resource_link`. |
| A string | One `user` message with that text. |
| An array of `{ role, content }` messages | Those messages, as if you had returned `{ messages }`. |
| Any other object | One `user` text message with the object as JSON. |

The result's `description` is the one you return, or else the prompt's `description` option. FrontMCP checks the result when `execute()` returns: a message whose `role` isn't `"user"` or `"assistant"` [fails the request](#tool-output-validation-failed-from-a-prompt).

Inside `execute()`, `this` is the `PromptContext`, with the members [every context class](https://frontmcp.dev/reference/sdk/contexts#members-every-class-shares) has: `this.get()` and `this.tryGet()` for [providers](https://frontmcp.dev/reference/sdk/provider), [`this.auth`](https://frontmcp.dev/reference/sdk/auth) for who is asking, [`this.context`](https://frontmcp.dev/reference/sdk/context#reading-the-request-in-a-prompt) for the request, [`this.fetch()`](https://frontmcp.dev/reference/sdk/fetch) and [`this.callTool()`](https://frontmcp.dev/reference/sdk/call-tool). It adds `this.args` for the arguments, and `this.metadata` for the options above. (Changed in 1.9.2: before, `PromptContext` had none of `this.auth`, `this.context`, `this.fetch()` and `this.callTool()`, and the caller was in `this.authInfo`, now deprecated.)

#### Caveats

- The class **must extend `PromptContext`**. Using `@Prompt` on any other class is a compile error.
- Argument values are **always strings**, because that's all MCP allows. Convert them yourself.
- Arguments that aren't declared are **passed through** to `execute()`, not dropped as they are for tools.
- When `execute()` throws a `PublicMcpError` (or a subclass, like `InvalidInputError`), the client gets a JSON-RPC error, code `-32602`, whose message is your error's message. [See rejecting bad arguments.](#rejecting-bad-arguments)
- Any other error is an internal one: code `-32603`, and `Prompt execution failed: ` followed by its message in development. With `NODE_ENV=production`, FrontMCP hides the message and sends `Internal FrontMCP error. Please contact support with error ID: …` instead.
- `this.fail(error)` fails the request the same way as `throw error` for a `PublicMcpError`. For any other error it's still `-32603`, but the message is your error's message without the `Prompt execution failed: ` prefix, and `data.code` is `SERVER_ERROR` instead of `PROMPT_EXECUTION_FAILED`. Production hides it the same way.
- [`this.respond(value)`](https://frontmcp.dev/reference/sdk/respond) ends `execute()` as `return value` would. (Changed in 1.9.3: before, the request failed with [`Prompt output not found`](#prompt-execution-failed-prompt-output-not-found).)
- Declaring `execute()` as returning `Promise<GetPromptResult>` is optional. With it, TypeScript checks each message's `role` and `content` as you write them.

---

## Usage

### Declaring arguments

List every value the user should fill in. Clients read `arguments` from `prompts/list` to build the form, so describe each one. Open the **Capabilities** tab to see the prompt the way a client does, and the **Tests** tab to see what `required` does.

```ts triage-ticket.prompt.ts active
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({
  name: "triage_ticket",
  title: "Triage a ticket",
  description: "Suggest a priority and a next step for a support ticket",
  arguments: [
    { name: "id", description: "Ticket id, like T-1", required: true },
    { name: "tone", description: "friendly or formal (default: formal)" },
  ],
})
export class TriageTicket extends PromptContext {
  async execute(args: Record<string, string>): Promise<GetPromptResult> {
    const tone = args.tone ?? "formal";
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Read ticket ${args.id}. Suggest a priority (low, normal or high) and a next step, in a ${tone} tone.`,
          },
        },
      ],
    };
  }
}
```

```ts triage.test.ts
import { test, expect } from "@frontmcp/testing";

test("clients see which arguments are required", async ({ mcp }) => {
  const prompt = (await mcp.prompts.list()).find((p) => p.name === "triage_ticket");
  expect(prompt?.arguments).toEqual([
    { name: "id", description: "Ticket id, like T-1", required: true },
    { name: "tone", description: "friendly or formal (default: formal)" },
  ]);
});

test("leaving out a required argument is an error", async ({ mcp }) => {
  const result = await mcp.prompts.get("triage_ticket", {});
  expect(result).toBeError(-32602);
  expect(result.error?.message).toBe("Missing required argument: id");
});

test("optional arguments can be left out", async ({ mcp }) => {
  const result = await mcp.prompts.get("triage_ticket", { id: "T-1" });
  expect(result).toHaveMessages(1);
  expect(result.messages[0].content.text).toContain("in a formal tone");
});

test("undeclared arguments aren't rejected", async ({ mcp }) => {
  const result = await mcp.prompts.get("triage_ticket", { id: "T-1", colour: "blue" });
  expect(result).toBeSuccessful();
});
```

### Writing several messages

A prompt can start a conversation with more than one turn. An `assistant` message puts words in the model's mouth, which is a reliable way to set the format of its answer.

```ts draft-reply.prompt.ts
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({
  name: "draft_reply",
  title: "Draft a reply",
  description: "Draft a short reply to a customer",
  arguments: [
    { name: "customer", description: "The customer's first name", required: true },
    { name: "problem", description: "What they reported", required: true },
  ],
})
export class DraftReply extends PromptContext {
  async execute({ customer, problem }: Record<string, string>): Promise<GetPromptResult> {
    return {
      description: `Reply to ${customer}`,
      messages: [
        { role: "user", content: { type: "text", text: `You're a support agent. Keep replies under 80 words and end with one clear next step.` } },
        { role: "assistant", content: { type: "text", text: "Understood. What did the customer report?" } },
        { role: "user", content: { type: "text", text: `${customer} wrote: "${problem}". Draft the reply.` } },
      ],
    };
  }
}
```

The result's `description` is the one you return. Without one, FrontMCP uses the prompt's `description` option.

### Returning a string or an array

A prompt that's a single user message can return just its text. A prompt with several messages can return the array of messages, without the `{ messages }` around it. Open the **Tests** tab to see what each one sends.

```ts shortcuts.prompt.ts active
import { Prompt, PromptContext } from "@frontmcp/sdk";

@Prompt({
  name: "escalation_note",
  description: "Write an escalation note for the on-call engineer",
  arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class EscalationNote extends PromptContext {
  async execute({ id }: Record<string, string>) {
    return `Write a two-line escalation note for ticket ${id}: impact first, then what was tried.`;
  }
}

@Prompt({ name: "reply_in_house_style", description: "Draft a reply in the help desk's style" })
export class ReplyInHouseStyle extends PromptContext {
  async execute() {
    return [
      { role: "user", content: { type: "text", text: "Draft replies in two sentences: what we did, then what happens next." } },
      { role: "assistant", content: { type: "text", text: "Understood. Paste the ticket." } },
    ];
  }
}
```

```ts shortcuts.test.ts
import { test, expect } from "@frontmcp/testing";

test("a string is one user message", async ({ mcp }) => {
  const result = await mcp.prompts.get("escalation_note", { id: "T-3" });
  expect(result.messages).toEqual([
    { role: "user", content: { type: "text", text: "Write a two-line escalation note for ticket T-3: impact first, then what was tried." } },
  ]);
});

test("the prompt's description comes along", async ({ mcp }) => {
  const result = await mcp.prompts.get("escalation_note", { id: "T-3" });
  expect(result.description).toBe("Write an escalation note for the on-call engineer");
});

test("an array is the list of messages", async ({ mcp }) => {
  const result = await mcp.prompts.get("reply_in_house_style", {});
  expect(result.messages.map((m) => m.role)).toEqual(["user", "assistant"]);
  expect(result.messages[0].content.text).toContain("two sentences");
});
```

### Including data in a message

A message can carry data as well as text. Embed it, or link to a [resource](https://frontmcp.dev/reference/sdk/resource) the client can read.

<Examples title="Data in a message">

#### Example: Embedded resource
Get the data from a [provider](https://frontmcp.dev/reference/sdk/provider) with `this.get()`, and embed it as a `resource` block. The client sends the model the data itself, with its URI and MIME type.

```ts summarize-ticket.prompt.ts active
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";

@Prompt({
  name: "summarize_ticket",
  description: "Summarize a ticket and its history for a handover",
  arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class SummarizeTicket extends PromptContext {
  async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
    const ticket = this.get(TicketStore).find(id);
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "resource",
            resource: { uri: `ticket://${id}`, mimeType: "application/json", text: JSON.stringify(ticket) },
          },
        },
        { role: "user", content: { type: "text", text: "Summarize this ticket in three bullet points for the next agent." } },
      ],
    };
  }
}
```

```ts ticket-store.ts
import { Provider } from "@frontmcp/sdk";

@Provider({ name: "TicketStore" })
export class TicketStore {
  private tickets = [
    { id: "T-1", title: "Cannot log in", status: "open", history: ["Reported by Amira", "Password reset sent"] },
    { id: "T-2", title: "Invoice total is wrong", status: "closed", history: ["Refund issued"] },
  ];
  find(id: string) {
    return this.tickets.find((t) => t.id === id);
  }
}
```

#### Example: Resource link
A `resource_link` block sends only the URI. The client reads the resource itself with `resources/read` if it needs it, which keeps large data out of the prompt. Here the link points at a resource template in the same server; use the **Request** menu to read `ticket://T-1`.

```ts summarize-ticket.prompt.ts active
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({
  name: "summarize_ticket",
  description: "Summarize a ticket and its history for a handover",
  arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class SummarizeTicket extends PromptContext {
  async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
    return {
      messages: [
        { role: "user", content: { type: "resource_link", uri: `ticket://${id}`, name: id, mimeType: "application/json" } },
        { role: "user", content: { type: "text", text: "Read the linked ticket, then summarize it in three bullet points." } },
      ],
    };
  }
}
```

```ts ticket.resource.ts
import { ResourceContext, ResourceTemplate } from "@frontmcp/sdk";

@ResourceTemplate({ name: "ticket", uriTemplate: "ticket://{id}", mimeType: "application/json", description: "One ticket by id" })
export class TicketResource extends ResourceContext<{ id: string }> {
  async execute(uri: string, { id }: { id: string }) {
    const ticket = { id, title: "Cannot log in", status: "open" };
    return { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(ticket) }] };
  }
}
```

### Rejecting bad arguments

Arguments are free text, so a prompt can't declare a format the way a tool's schema can. Check the values in `execute()` and throw a `PublicMcpError` with a message the user can act on. The client gets a JSON-RPC error, code `-32602`, with your message.

```ts summarize-ticket.prompt.ts active
import { Prompt, PromptContext, PublicMcpError, type GetPromptResult } from "@frontmcp/sdk";

const tickets = [{ id: "T-1", title: "Cannot log in", status: "open" }];

@Prompt({
  name: "summarize_ticket",
  description: "Summarize a ticket for a handover",
  arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class SummarizeTicket extends PromptContext {
  async execute({ id }: Record<string, string>): Promise<GetPromptResult> {
    const ticket = tickets.find((t) => t.id === id);
    if (!ticket) throw new PublicMcpError(`There's no ticket ${id}. Ticket ids look like T-1.`);
    return { messages: [{ role: "user", content: { type: "text", text: `Summarize: ${ticket.title} (${ticket.status})` } }] };
  }
}
```

```ts summarize.test.ts
import { test, expect } from "@frontmcp/testing";

test("the user gets the message as it was thrown", async ({ mcp }) => {
  const result = await mcp.prompts.get("summarize_ticket", { id: "T-9" });
  expect(result).toBeError(-32602);
  expect(result.error?.message).toBe("There's no ticket T-9. Ticket ids look like T-1.");
});
```

> **Note**
Only a `PublicMcpError` keeps its message everywhere. Any other error thrown from a prompt is internal: in development the client sees `Prompt execution failed: ` and its message, and with `NODE_ENV=production` an error ID instead. So errors from databases and other services don't reach the user, and the user doesn't learn what went wrong either. Catch the ones you expect, and throw a `PublicMcpError` of your own.

### Writing a prompt as a function

For a prompt that doesn't need providers, the function form is shorter. It takes the same options, and the handler receives the arguments.

```ts escalate.prompt.ts
import { prompt } from "@frontmcp/sdk";

export const escalate = prompt({
  name: "escalate",
  description: "Write an escalation note for the on-call engineer",
  arguments: [{ name: "id", description: "Ticket id", required: true }],
})((args) => ({
  messages: [
    { role: "user", content: { type: "text", text: `Write a two-line escalation note for ticket ${args?.id}: impact first, then what was tried.` } },
  ],
}));
```

---

## Troubleshooting

### `Missing required argument: id`

The client asked for the prompt without a value for an argument marked `required: true`, so `execute()` didn't run. The response is a JSON-RPC error with code `-32602`. If users often skip the argument, say more about it in its `description`, or make it optional and use a default in `execute()`.

### `Prompt not found: triage_ticket`

No prompt with that name is registered on the server, and the response is a JSON-RPC error with code `-32602`. Check, in order:

1. The class is in its app's `prompts` array (not `tools`), and the app is in `@FrontMcp({ apps })`. `@FrontMcp` has no `prompts` option; one there is silently ignored.
2. The name matches exactly, including case.
3. The prompt's `availableWhen.surface`, if it has one, includes `"mcp"`. MCP clients get this error for a prompt that leaves it out.

### The plain name gets another app's prompt

Two apps declare a prompt with the same name. `prompts/list` shows them as `<app id>:<name>`, like `desk:triage_ticket` and `billing:triage_ticket`, and `prompts/get` finds each by that name. The plain name, `triage_ticket`, gets the first app's prompt. Use the names `prompts/list` gives:

```ts main.ts
import { App, FrontMcp, Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";

@Prompt({ name: "triage_ticket", arguments: [] })
class DeskTriage extends PromptContext {
  async execute(): Promise<GetPromptResult> {
    return { messages: [{ role: "user", content: { type: "text", text: "Triage the newest support ticket." } }] };
  }
}

@Prompt({ name: "triage_ticket", arguments: [] })
class BillingTriage extends PromptContext {
  async execute(): Promise<GetPromptResult> {
    return { messages: [{ role: "user", content: { type: "text", text: "Triage the newest billing dispute." } }] };
  }
}

@App({ id: "desk", name: "Help Desk", prompts: [DeskTriage] })
class DeskApp {}

@App({ id: "billing", name: "Billing", prompts: [BillingTriage] })
class BillingApp {}

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [DeskApp, BillingApp] })
export default class Server {}
```

```ts names.test.ts
import { test, expect } from "@frontmcp/testing";

test("each app's prompt by its listed name; the plain name gets the first app's", async ({ mcp }) => {
  expect((await mcp.prompts.list()).map((prompt) => prompt.name)).toEqual(["billing:triage_ticket", "desk:triage_ticket"]);
  const expected = {
    "desk:triage_ticket": "Triage the newest support ticket.",
    "billing:triage_ticket": "Triage the newest billing dispute.",
    triage_ticket: "Triage the newest support ticket.",
  };
  for (const [name, text] of Object.entries(expected)) {
    expect((await mcp.prompts.get(name, {})).messages[0].content.text).toBe(text);
  }
});
```

Give prompts names that are unique across the whole server, like `triage_billing_dispute`, so clients don't need the prefix. (Changed in 1.9.2: before, `prompts/get` answered `Prompt not found: billing:triage_ticket`, and the second app's prompt couldn't be reached.)

### `Prompt "…" is not available in the current environment`

The prompt's `availableWhen` doesn't match where the server runs. It's left out of `prompts/list`, and `prompts/get` fails with a JSON-RPC error with code `-32003`, whose message says what the prompt requires and what the server is:

```ts deploy-checklist.prompt.ts active
import { Prompt, PromptContext } from "@frontmcp/sdk";

@Prompt({ name: "deploy_checklist", description: "Walk through a Deno Deploy release", availableWhen: { runtime: ["deno"] } })
export class DeployChecklist extends PromptContext {
  async execute() {
    return "List what to check before running deployctl.";
  }
}
```

```ts deploy.test.ts
import { test, expect } from "@frontmcp/testing";

test("it isn't listed, and getting it fails", async ({ mcp }) => {
  expect(await mcp.prompts.list()).toEqual([]);
  const result = await mcp.prompts.get("deploy_checklist", {});
  expect(result).toBeError(-32003);
  expect(result.error?.message).toMatch(/^Prompt "deploy_checklist" is not available in the current environment \(requires: \{"runtime":\["deno"\]\}\)/);
});
```

The message lists the server's OS, runtime and deployment, in production too. If the prompt should be offered here, fix its `availableWhen`.

### `Prompt execution failed: Prompt output not found`

`execute()` finished without returning anything, or returned `undefined`. Here a lookup misses for every ticket but `T-1`:

```ts summarize-ticket.prompt.ts
import { Prompt, PromptContext } from "@frontmcp/sdk";

const summaries: Record<string, string> = { "T-1": "Summarize ticket T-1, our oldest." };

@Prompt({ name: "summarize_ticket", arguments: [{ name: "id", required: true }] })
export class SummarizeTicket extends PromptContext {
  async execute({ id }: Record<string, string>) {
    return summaries[id]; // 🚩 undefined for T-2
  }
}
```

Return a result on every path:

```ts
// ✅ Every id gets a message
return summaries[id] ?? `Summarize ticket ${id}.`;
```

Before 1.9.3, a prompt that called `this.respond()` failed with this error too. It now ends the request with the value it's given.

### `Tool output validation failed`, from a prompt

A message has a `role` other than `"user"` or `"assistant"`, usually `"system"`. MCP prompts have no system role, so FrontMCP rejects the result. The client gets error `-32603`, with `INVALID_OUTPUT` as the code in its `data`, and a message that says "Tool" even though this is a prompt. What failed validation is only in the server log:

```ts persona.prompt.ts
import { Prompt, PromptContext } from "@frontmcp/sdk";

@Prompt({ name: "support_persona" })
export class SupportPersona extends PromptContext {
  async execute() {
    return { messages: [{ role: "system", content: { type: "text", text: "You are a patient support agent." } }] };
  }
}
```

Put instructions in the first `user` message instead. Declaring `execute()` as returning `Promise<GetPromptResult>` catches this while you write it: TypeScript only accepts `"user"` and `"assistant"` there.

### TypeScript says `Property 'execute' in type '…' is not assignable to the same property in base type 'PromptContext'`

`execute()` can finish without returning anything, usually because a code path has no `return`. It must return a string, an array or an object, or throw, on every path:

```ts
// 🚩 Promise<void> on the path where the ticket exists
async execute({ id }: Record<string, string>) {
  if (!tickets.has(id)) throw new PublicMcpError(`There's no ticket ${id}.`);
}

// ✅ Every path returns or throws
async execute({ id }: Record<string, string>) {
  if (!tickets.has(id)) throw new PublicMcpError(`There's no ticket ${id}.`);
  return `Summarize ticket ${id}.`;
}
```
