Reusable Prompts
A prompt is a message template that the user picks, usually from a slash menu in their client. It's the third kind of MCP capability: the model decides when to call a tool, the application decides which resources to read, and the user decides when to run a prompt. Prompts are how you ship the instructions that work best with your server, so nobody has to write them from memory.
You will learn
- What a prompt is, and how it differs from tools and resources
- How to declare a
@Promptwith required and optional arguments - How to put a ticket's data into a prompt as an embedded resource
- How to return several messages, including an example exchange
- How clients show prompts to the people using them
A workflow the user starts
Every support agent triages tickets a little differently. One asks the model for a priority, another for a team, a third forgets to ask for a reply at all. When there's a way that works, put it on the server as a prompt:
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";
@Prompt({
name: "triage",
title: "Triage a ticket",
description: "Suggest a priority, an owning team and a first reply for a support ticket",
})
export class Triage extends PromptContext {
async execute(): Promise<GetPromptResult> {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Triage the support ticket I paste next. Suggest a priority (low, normal, high or urgent), the team that should own it, and a two-sentence first reply to the customer.",
},
},
],
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Open the Call tab. The page sent prompts/get, and what came back is a list of messages, not an answer. Getting a prompt doesn't run the model. The client puts these messages into the conversation as if the user had typed them, and the model replies from there, calling your tools if it needs to.
A @Prompt has:
name, what clients use to get it. Like tool names, usesnake_case.titleanddescription, what the user sees in the client's menu. Unlike a tool's description, these are written mostly for people.arguments, the values the user fills in. This one has none yet, so it leaves the option out.- The class, which extends
PromptContextand returns{ messages }fromexecute(). Each message has arole("user"or"assistant") and one piece ofcontent.
The @Prompt reference lists every option.
So the three kinds of capability differ in who decides to use them:
| Tool | Resource | Prompt | |
|---|---|---|---|
| Who decides | The model | The application, often because the user attached it | The user |
| Identified by | A name | A URI | A name |
| Used with | tools/call | resources/read | prompts/get |
| Gives back | A result for the model | Content for the conversation | Messages to start the conversation with |
Arguments
The prompt above makes the user paste the ticket in a second message. It's easier to ask for the ticket id up front:
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";
@Prompt({
name: "triage_ticket",
title: "Triage a ticket",
description: "Suggest a priority, an owning team and a first reply for one support ticket",
arguments: [
{ name: "id", description: "Ticket id, like T-1", required: true },
{ name: "language", description: "Language for the reply to the customer. Defaults to English." },
],
})
export class TriageTicket extends PromptContext {
async execute({ id, language }: { id: string; language?: string }): Promise<GetPromptResult> {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Look up ticket ${id} with get_ticket, then triage it. Suggest a priority (low, normal, high or urgent), the team that should own it, and a two-sentence first reply in ${language ?? "English"}.`,
},
},
],
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
Each argument has a name, a description for the person filling it in, and required:
- Required arguments must be sent. Clients ask for them before they send
prompts/get, and FrontMCP rejects a request without them. - Optional arguments can be left out. A missing one is
undefinedinexecute(), so give it a default, likelanguage ?? "English".
Here is the same prompt, requested without an id:
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";
@Prompt({
name: "triage_ticket",
title: "Triage a ticket",
description: "Suggest a priority, an owning team and a first reply for one support ticket",
arguments: [
{ name: "id", description: "Ticket id, like T-1", required: true },
{ name: "language", description: "Language for the reply to the customer. Defaults to English." },
],
})
export class TriageTicket extends PromptContext {
async execute({ id, language }: { id: string; language?: string }): Promise<GetPromptResult> {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Look up ticket ${id} with get_ticket, then triage it. Suggest a priority (low, normal, high or urgent), the team that should own it, and a two-sentence first reply in ${language ?? "English"}.`,
},
},
],
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The request fails with a JSON-RPC error, code -32602 (invalid params) and the message Missing required argument: id, and execute() never runs. That's the only check FrontMCP makes. Prompt arguments are plain strings in MCP, with no schema like a tool's inputSchema, so an id like banana gets through to your code. If a value matters, check it yourself.
Putting the ticket in the prompt
"Look up ticket T-1 with get_ticket" leaves a step to the model: it has to call the tool before it can start. Since the prompt runs on your server, it can look the ticket up itself and send the data along. The clearest way to send data is as an embedded resource: a message whose content is a resource, with a URI, instead of text:
import { Prompt, PromptContext, PublicMcpError, type GetPromptResult } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";
@Prompt({
name: "triage_ticket",
title: "Triage a ticket",
description: "Suggest a priority, an owning team and a first reply for one support ticket",
arguments: [
{ name: "id", description: "Ticket id, like T-1", required: true },
{ name: "language", description: "Language for the reply to the customer. Defaults to English." },
],
})
export class TriageTicket extends PromptContext {
async execute({ id, language }: { id: string; language?: string }): Promise<GetPromptResult> {
const ticket = this.get(TicketStore).get(id);
if (!ticket) throw new PublicMcpError(`There's no ticket ${id}.`);
return {
messages: [
{
role: "user",
content: {
type: "resource",
resource: { uri: `tickets://${ticket.id}`, mimeType: "application/json", text: JSON.stringify(ticket) },
},
},
{
role: "user",
content: {
type: "text",
text: `Triage the ticket above. Suggest a priority (low, normal, high or urgent), the team that should own it, and a two-sentence first reply in ${language ?? "English"}.`,
},
},
],
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
A few things changed:
- The prompt gets the ticket from a provider.
PromptContexthasthis.get(), like tools and resources, so the prompt and thetickets://{id}template read the sameTicketStore. - The first message is the ticket itself, as
{ type: "resource", resource: { uri, mimeType, text } }. The URI says where the data came from, and it's the same URI the ticket template serves. Clients can show it as an attached ticket instead of a wall of JSON the user seems to have typed, and the model can tell data apart from instructions. - The instructions come second, as text, and refer to "the ticket above".
- An unknown id fails.
throw new PublicMcpError(...)turns into a JSON-RPC error, code-32602, whose message is yours, so the client can tell the user thatT-9doesn't exist. Open the Tests tab to see all three.
Several messages
A prompt can return more than one message, and they don't all have to be from the user. A message with role: "assistant" is a turn the model appears to have already taken. The most useful thing to do with that is show the model an example of the answer you want:
import { Prompt, PromptContext, PublicMcpError, type GetPromptResult } from "@frontmcp/sdk";
import { TicketStore } from "./ticket-store";
const instructions =
"Triage support tickets. Answer with a priority (low, normal, high or urgent), the team that should own the ticket, and a two-sentence reply to the customer.";
const exampleTicket = { id: "T-0", title: "Password reset email never arrives", status: "open", customer: "Initech" };
const exampleAnswer = [
"Priority: high",
"Team: Accounts",
"Reply: Sorry the reset email hasn't reached you. We've sent a fresh link, and if it isn't in your inbox within ten minutes, please check your spam folder.",
].join("\n");
@Prompt({
name: "triage_ticket",
title: "Triage a ticket",
description: "Suggest a priority, an owning team and a first reply for one support ticket",
arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class TriageTicket extends PromptContext {
async execute({ id }: { id: string }): Promise<GetPromptResult> {
const ticket = this.get(TicketStore).get(id);
if (!ticket) throw new PublicMcpError(`There's no ticket ${id}.`);
return {
messages: [
{ role: "user", content: { type: "text", text: `${instructions}\n\n${JSON.stringify(exampleTicket)}` } },
{ role: "assistant", content: { type: "text", text: exampleAnswer } },
{
role: "user",
content: {
type: "resource",
resource: { uri: `tickets://${ticket.id}`, mimeType: "application/json", text: JSON.stringify(ticket) },
},
},
],
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.
The client adds all three messages to the conversation in order: the instructions with a made-up ticket, a model turn that answers it in exactly the format you want, and then the real ticket. The model continues the pattern. An example like this pins down the format far better than describing it, and it costs you one extra message.
How clients show prompts
Clients discover prompts with prompts/list, and most show them as slash commands or in a menu of actions. When the user picks one, the client asks for its arguments, then sends prompts/get with the answers and puts the messages it gets back into the conversation.
What the user sees comes from your metadata:
titleis the label in the menu. Without one, clients fall back toname.descriptionsays what the prompt will do, so the user can pick the right one.- Each argument's
descriptionis the hint next to its input, andrequireddecides whether the user can skip it.
The Playground works the same way. On the Capabilities tab, a prompt card lists its arguments, with ? after optional ones, and the Call tab turns them into a form.
Deep diveWhat does the client actually receive?Show detailsHide details
For the triage_ticket prompt with an id and a language, prompts/list returns:
{
"prompts": [
{
"name": "triage_ticket",
"title": "Triage a ticket",
"description": "Suggest a priority, an owning team and a first reply for one support ticket",
"arguments": [
{ "name": "id", "description": "Ticket id, like T-1", "required": true },
{ "name": "language", "description": "Language for the reply to the customer. Defaults to English." }
]
}
]
}An argument you didn't mark required is sent without the field, which MCP reads as optional. The client then sends prompts/get with { "name": "triage_ticket", "arguments": { "id": "T-2" } }, and gets back the messages exactly as execute() returned them. If you don't return a description of your own, FrontMCP adds the prompt's. Open the Wire tab of any example above to see the full exchange.
Recap
- A prompt is a message template the user picks. Getting it returns messages for the conversation; it doesn't run the model.
- A
@Prompthas aname, atitleanddescriptionfor people,arguments, and a class that extendsPromptContextand returns{ messages }. - Required arguments must be sent, or
prompts/getfails with-32602. Optional ones areundefinedwhen left out. FrontMCP checks nothing else about them. - Put data in a prompt as an embedded resource (
type: "resource") with a URI, and get it from the same provider your resources use. - Messages can alternate between
userandassistant. An example exchange shows the model the answer format you want. - Throw
PublicMcpErrorfor problems the user should see. A plainErroris a server failure, and production hides its message.
Try some challenges
Each challenge runs hidden checks against your code. Edit the code, then press Check.
Challenge 1 of 3
Add an optional tone
The draft_reply prompt always asks for a friendly reply. Some customers need a formal one. Add an optional tone argument, with a description, that can be friendly or formal, and use friendly when it's left out.
import { Prompt, PromptContext, type GetPromptResult } from "@frontmcp/sdk";
@Prompt({
name: "draft_reply",
title: "Draft a reply",
description: "Draft a reply to the customer on a ticket",
arguments: [{ name: "id", description: "Ticket id, like T-1", required: true }],
})
export class DraftReply extends PromptContext {
async execute({ id }: { id: string }): Promise<GetPromptResult> {
return {
messages: [
{
role: "user",
content: { type: "text", text: `Look up ticket ${id} with get_ticket and draft a friendly reply to the customer.` },
},
],
};
}
}Starting FrontMCP in your browser…
FrontMCP starts when this example comes into view.