Prompts
A prompt is a message template a server offers and a user picks, filled with a few named arguments.
Last updated: 29 Sep, 2026 · MCP 2.2
Tools are for the model and resources are for the application. A prompt is for the user: support agents write the same kinds of reply all day, so the server keeps a good starting point and a person picks it from a menu.
The @mcp.prompt decorator
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, UserMessage
@mcp.prompt(title="Reply to a customer")
def reply_to_customer(ticket: str, tone: str = "friendly") -> list[Message]:
return [UserMessage(...), AssistantMessage(...)]The reply_to_customer prompt
@mcp.prompt() registers a prompt. Returning a list of messages seeds a conversation; returning a plain string would give one user message. The imports gain Message, UserMessage and AssistantMessage from mcp.server.mcpserver.prompts.base.
@mcp.prompt(title="Reply to a customer")
def reply_to_customer(ticket: str, tone: str = "friendly") -> list[Message]:
"""Draft a reply to a support ticket."""
return [
UserMessage(f"Write a {tone} reply to this support ticket:\n\n{ticket}"),
AssistantMessage("Hello, and thank you for getting in touch."),
]The last message is from the assistant. Pre-filling the start of the reply steers how the model continues it, with no extra instructions from the user.
View the code here
from typing import Annotated, Literal
from pydantic import BaseModel, Field
from mcp.server import MCPServer
from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, UserMessage
from mcp.server.mcpserver.exceptions import ResourceNotFoundError, ToolError
from mcp.types import ToolAnnotations
mcp = MCPServer("Shop support")
ORDERS = {
"A17": {"item": "blue mug", "status": "shipped", "total": 12.50},
"B42": {"item": "desk lamp", "status": "processing", "total": 48.00},
}
ARTICLES = {
"Where is my order?": "orders",
"Changing an order": "orders",
"How refunds work": "refunds",
"Refunds for damaged items": "refunds",
"Resetting your password": "account",
}
class Order(BaseModel):
id: str
item: str
status: Literal["processing", "shipped", "delivered"]
total: float
@mcp.tool(annotations=ToolAnnotations(read_only_hint=True))
def lookup_order(order_id: str) -> Order:
"""Look up an order by its id."""
if order_id not in ORDERS:
raise ToolError(f"No order with id {order_id!r}. Order ids look like A17.")
return Order(id=order_id, **ORDERS[order_id])
@mcp.tool()
def search_help(
query: Annotated[str, Field(description="Words to look for in the help articles.")],
topic: Literal["orders", "refunds", "account"] | None = None,
limit: Annotated[int, Field(ge=1, le=5)] = 3,
) -> str:
"""Search the help centre and return matching article titles."""
found = [title for title, t in ARTICLES.items() if query.lower() in title.lower() and topic in (None, t)]
return "; ".join(found[:limit]) or "No articles found."
@mcp.tool(
title="Refund an order",
annotations=ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=False),
)
def refund_order(order_id: str, reason: str) -> str:
"""Refund the full total of an order to the customer's card."""
if order_id not in ORDERS:
raise ToolError(f"No order with id {order_id!r}.")
return f"Refunded {ORDERS[order_id]['total']:.2f} for order {order_id}: {reason}."
@mcp.resource("policy://refunds", mime_type="text/markdown")
def refund_policy() -> str:
"""The shop's refund policy."""
return "# Refunds\n\nFull refund within 30 days of delivery. Damaged items: refund or replacement."
@mcp.resource("orders://{order_id}", mime_type="application/json")
def order_record(order_id: str) -> Order:
"""The full record for one order."""
if order_id not in ORDERS:
raise ResourceNotFoundError(f"No order with id {order_id!r}.")
return Order(id=order_id, **ORDERS[order_id])
@mcp.prompt(title="Reply to a customer")
def reply_to_customer(ticket: str, tone: str = "friendly") -> list[Message]:
"""Draft a reply to a support ticket."""
return [
UserMessage(f"Write a {tone} reply to this support ticket:\n\n{ticket}"),
AssistantMessage("Hello, and thank you for getting in touch."),
]
Listing and rendering a prompt
Listing shows the arguments a user fills; rendering fills them in.
import asyncio
from mcp import Client
from shop import mcp
async def main():
async with Client(mcp) as client:
listed = await client.list_prompts()
prompt = listed.prompts[0]
print(prompt.name, "/", prompt.title)
print([(argument.name, argument.required) for argument in prompt.arguments])
result = await client.get_prompt("reply_to_customer", {"ticket": "My mug arrived broken"})
for message in result.messages:
print(message.role, "->", message.content.text)
asyncio.run(main())reply_to_customer / Reply to a customer
[('ticket', True), ('tone', False)]
user -> Write a friendly reply to this support ticket:
My mug arrived broken
assistant -> Hello, and thank you for getting in touch.What the render produced
- The arguments are a flat list of named strings, not a JSON Schema: they fill a form a person sees.
tonehas a default, so it is not required, andticketis.get_promptrendered two messages, which the application adds to the chat.
Three kinds of thing, one server
| Tool | Resource | Prompt | |
|---|---|---|---|
| Chosen by | The model | The application | The user |
| Arguments | JSON Schema | The URI | A flat form |
| In Claude Code | Callable in a run | Attachable data | A slash command |
When to add a prompt
- A reply or a report a team writes again and again with small changes.
- A starting point you want to pin on the server so everyone uses the same one.
Related
- Previous: Resource templates
- Next: Context
- Reference: MCP server concepts
- Render the prompt with
"tone": "formal". - Call
get_promptwithoutticketand read the error. - Add a prompt
summarise_order(order_id: str) -> strthat returns one user message.
You understood something today that you didn't yesterday.