Model Context ProtocolMCP Python SDK 2.2 · LangChain 1.4 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson →

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

python
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.

python
@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.

Project files used on this pageThis lesson builds on a project from earlier lessons. The code below imports this file. Click a file to see its code, or follow the link to the lesson that wrote it. To run the code yourself, keep it in the same folder.
View the code here
shop.py
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.

Example
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())

What the render produced

  • The arguments are a flat list of named strings, not a JSON Schema: they fill a form a person sees.
  • tone has a default, so it is not required, and ticket is.
  • get_prompt rendered two messages, which the application adds to the chat.

Three kinds of thing, one server

ToolResourcePrompt
Chosen byThe modelThe applicationThe user
ArgumentsJSON SchemaThe URIA flat form
In Claude CodeCallable in a runAttachable dataA 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.
Watch out. Prompt arguments are plain strings in a form, not a validated schema like a tool's. If an argument must be one of a few values, check it inside the function, because the SDK will not reject a wrong string for you.
Try it yourself
  • Render the prompt with "tone": "formal".
  • Call get_prompt without ticket and read the error.
  • Add a prompt summarise_order(order_id: str) -> str that returns one user message.

You understood something today that you didn't yesterday.