Prompts are the Model Context Protocol primitive most servers skip, and it shows. If your server exposes tools but every client has to hand-write the wording that drives them, you have shipped half a product. A prompt is a named, argument-driven message template that the server owns and the client surfaces to the user, usually as a slash command. To add one you declare the prompts capability, answer two methods (prompts/list and prompts/get), and in the Python SDK that whole contract collapses into a single @mcp.prompt() decorator.
Key Takeaways
- Short answer: decorate a function with
@mcp.prompt(). Its parameters become the prompt's arguments, and its return value becomes the message or messages the host feeds to the model. - Prompts are user-controlled. The host surfaces them for a person to pick, which is the opposite of tools, where the model decides to call them.
- A server advertises support with the
promptscapability during initialization. SetlistChanged: trueonly if you will actually emitnotifications/prompts/list_changed. - The wire protocol is two methods:
prompts/listfor discovery (paginated) andprompts/get, which returns adescriptionplus amessagesarray. - Message content can be text, image, audio, or an embedded
resource, and each argument can be auto-completed through the completion API.
What You Need Before You Start
Keep the surface small. This walkthrough uses the official Python SDK, which bundles the FastMCP framework, so one dependency covers everything.
- Python 3.10 or newer, which the MCP Python SDK on GitHub requires.
- The SDK with its CLI extra:
pip install "mcp[cli]". - An MCP host that renders prompts, such as Claude Desktop, or the MCP Inspector for local testing.
Add a Prompt to Your MCP Server in Five Steps
Work top to bottom. When you finish step five you have a server that lists two prompts and returns real messages for each.
- Install the SDK.
pip install "mcp[cli]" - Create the server. FastMCP declares the
promptscapability for you the moment you register a prompt, so there is no capability boilerplate to write.from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.prompts import base mcp = FastMCP("prompt-demo") - Add a single-message prompt with an argument. The function parameter
codebecomes a required prompt argument because it has no default. Returning a plain string produces oneusermessage.@mcp.prompt() def review_code(code: str) -> str: """Ask the model to review a snippet and suggest fixes.""" return f"Please review this code and suggest improvements:\n\n{code}" - Add a multi-message prompt with explicit roles. Return a list of typed messages when you want to prime a short exchange rather than a single instruction. An optional argument gets a default value.
@mcp.prompt() def debug_error(error: str, language: str = "python") -> list[base.Message]: """Seed a debugging conversation for a stack trace.""" return [ base.UserMessage(f"I hit this error in {language}:"), base.UserMessage(error), base.AssistantMessage("I'll help debug that. What did you expect to happen?"), ] - Run it. Add an entry point and launch the server over stdio, or open it in the Inspector.
if __name__ == "__main__": mcp.run() # quick manual test with the Inspector: # mcp dev server.py
That is the whole task. The reader who stops here has two working prompts. The rest of this guide explains what crossed the wire and how to fix it when a host refuses to show them.
What the Protocol Actually Sends
FastMCP hides the JSON-RPC, but knowing the shape helps when you debug a client or write your own. Discovery uses prompts/list, which returns each prompt's name, optional title and description, and an arguments array where every entry carries name, description, and required. The MCP prompts specification fixes those exact field names in the 2025-06-18 revision.
Retrieval uses prompts/get. The client sends the prompt name plus an arguments object, and the server returns a description and a messages array:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"description": "Code review prompt",
"messages": [
{
"role": "user",
"content": { "type": "text", "text": "Please review this Python code..." }
}
]
}
}Each message's content is one of four types the spec allows: text, image and audio (both base64 with a mimeType), or an embedded resource with its own uri. That last type is the bridge to your other primitives, so a prompt can inline a file or record your server already exposes. If you are wiring those up, see the companion guide on how to expose resources from an MCP server.
How Do Prompt Arguments Get Auto-Completed?
A good host offers argument suggestions as the user types. That is not magic in the prompt itself. It runs through the separate completion API, and the client calls completion/complete against a prompt reference to fetch candidate values. The mechanics and request shape live in the completion utility specification. Wire it up when an argument has a bounded set of sensible values, such as a project name or a known enum.
When Should the Prompt List Change at Runtime?
Most servers register their prompts once at startup and never touch the list again. If yours adds or removes prompts while running, declare listChanged: true and emit notifications/prompts/list_changed when the set changes, so clients re-fetch. Declaring the flag without ever sending the notification is worse than not declaring it, because clients trust the capability and cache a stale list.
Troubleshooting the Failures You Will Actually Hit
- The host shows tools but no prompts. Confirm the client supports the prompts primitive at all, since some surface only tools. Then restart the host after any config change, because most read the server list once at launch. If you build against the standalone FastMCP 2.x package instead of the bundled version, check its own prompts documentation, as the import path is
from fastmcp import FastMCPrather thanfrom mcp.server.fastmcp. - Error -32602, missing required arguments. A parameter with no default is required. Either give the client that argument or add a default value so the parameter becomes optional.
- Error -32602, invalid prompt name. The requested
namedid not match a registered prompt. The name is the function name unless you passedname=to the decorator, so keep the two in sync. - The prompt loads but the model ignores it. Prompts inject context that a person chose to insert. They do not force behavior the way a tool call does. If you need the model to take an action, expose a tool, not a prompt.
What to Do Next
You have prompts flowing. Three moves make the server production-ready:
- Round out the primitives. If you have not built the tools and resources yet, start from the full walkthrough on how to build an MCP server in Python with FastMCP.
- Add argument completion using the completion API so slash-command arguments suggest real values.
- Verify the list and get calls against a live client. The fastest path is the guide on how to debug an MCP server with the MCP Inspector, which drives
prompts/listandprompts/getby hand.