Prompts - MCP Python SDK
https://py.sdk.modelcontextprotocol.io/servers/prompts/ • 86 KB fetched
Open original page
Prompts - MCP Python SDK
Skip to content
MCP Python SDK
Prompts
*
en - English
*
de - Deutsch
*
es - español
*
fr - français
*
hi - हिन्दी
*
ja - 日本語
*
ko - 한국어
*
pt - português (Brasil)
*
ru - русский язык
*
tr - Türkçe
*
uk - українська мова
*
zh - 简体中文
*
zh-hant - 繁體中文
Search
modelcontextprotocol/python-sdk
MCP Python SDK
modelcontextprotocol/python-sdk
*
MCP Python SDK
*
What's new in v2
*
Get started
Get started
*
Installation
*
First steps
*
Connect to a real host
*
Testing
*
Servers
Servers
*
Tools
*
Structured Output
*
Resources
*
URI templates
*
Prompts
Prompts
On this page
*
Your first prompt
*
Rendering it
*
Try it
*
More than one message
*
Titles and argument descriptions
*
More than text
*
Embedding a file
*
Attaching an image
*
Changing the list at runtime
*
Recap
*
Completions
*
Images, audio & icons
*
Handling errors
*
Inside your handler
Inside your handler
*
The Context
*
Dependencies
*
Lifespan
*
Elicitation
*
Multi-round-trip requests
*
Sampling and roots
*
Progress
*
Logging
*
Subscriptions
*
Running your server
Running your server
*
Add to an existing app
*
Deploy & scale
*
Authorization
*
OpenTelemetry
*
Serving legacy clients
*
Clients
Clients
*
Callbacks
*
Transports
*
OAuth
*
Identity assertion
*
Multiple servers
*
Subscriptions
*
Caching
*
Protocol versions
*
Deprecated features
*
Advanced
Advanced
*
The low-level Server
*
Pagination
*
Middleware
*
Extensions
*
MCP Apps
*
Troubleshooting
*
Translations
*
Migration Guide
*
API Reference
API Reference
*
mcp
*
mcp_types
On this page
*
Your first prompt
*
Rendering it
*
Try it
*
More than one message
*
Titles and argument descriptions
*
More than text
*
Embedding a file
*
Attaching an image
*
Changing the list at runtime
*
Recap
*
MCP Python SDK
*
Servers
Prompts
A prompt is a message template the user picks.
Tools are for the model. A prompt is the opposite: the user chooses one from a menu in their client (a slash command, a button), fills in its arguments, and the rendered messages go into the conversation as if they had typed them.
You declare one by putting @mcp.prompt() on a function that returns the text.
Your first prompt
server.py from mcp.server import MCPServer
mcp = MCPServer ( "Code Helper" )
@mcp . prompt ()
def review_code ( code : str ) -> str :
"""Review a piece of code."""
return f "Please review this code: \n\n { code } "
The SDK reads the same three things it reads from a tool:
* The name is the function name: review_code .
* The description the client shows is the docstring: Review a piece of code.
* The arguments come from the parameters. code has no default, so it's required.
That is what a client gets back from prompts/list :
{
"name" : "review_code" ,
"description" : "Review a piece of code." ,
"arguments" : [
{ "name" : "code" , "required" : true }
]
}
There is no JSON Schema here. Prompt arguments are a flat list of named string values : a form a person fills in, not a payload a model constructs.
Rendering it
The client renders the template with prompts/get , passing the arguments. Your function runs and the str you return becomes one user message :
{
"description" : "Review a piece of code." ,
"messages" : [
{
"role" : "user" ,
"content" : {
"type" : "text" ,
"text" : "Please review this code:\n\ndef add(a, b): return a + b"
}
}
],
"resultType" : "complete"
}
That is the entire life of a prompt: listed by name, rendered on demand, dropped into the chat.
Check
required is enforced before your function runs. Render review_code without code and the
request itself fails with a JSON-RPC error (code -32603 ):
mcp.shared.exceptions.MCPError: Internal server error
There is no tool-style error result to hand back to a model, because no model is in the loop:
the call raises. The reason ( Missing required arguments: {'code'} ) lands in your server's log.
Try it
Run the server with the MCP Inspector:
uv run mcp dev server.py
Open the Prompts tab and select review_code . The Inspector draws a form with one required code field. Fill it in, render it, and you get back exactly the user message above.
More than one message
A code review is one message. A debugging session is a conversation, and a prompt can seed the whole thing.
Return a list of messages instead of a str :
server.py from mcp.server import MCPServer
from mcp.server.mcpserver.prompts.base import AssistantMessage , Message , UserMessage
mcp = MCPServer ( "Code Helper" )
@mcp . prompt ()
def review_code ( code : str ) -> str :
"""Review a piece of code."""
return f "Please review this code: \n\n { code } "
@mcp . prompt ()
def debug_error ( error : str ) -> list [ Message ]:
"""Start a debugging conversation."""
return [
UserMessage ( "I'm seeing this error:" ),
UserMessage ( error ),
AssistantMessage ( "I'll help debug that. What have you tried so far?" ),
]
* UserMessage and AssistantMessage come from mcp.server.mcpserver.prompts.base . Hand them a str and they wrap it in TextContent for you. The role is the class name.
* Message is their common base. Use it as the return annotation.
Rendering debug_error now produces three messages, in order:
{
"description" : "Start a debugging conversation." ,
"messages" : [
{ "role" : "user" , "content" : { "type" : "text" , "text" : "I'm seeing this error:" }},
{ "role" : "user" , "content" : { "type" : "text" , "text" : "TypeError: 'int' object is not iterable" }},
{
"role" : "assistant" ,
"content" : { "type" : "text" , "text" : "I'll help debug that. What have you tried so far?" }
}
],
"resultType" : "complete"
}
Notice the last one. Pre-filling an assistant turn is how you steer the model's next reply without making the user type the steering themselves.
Titles and argument descriptions
review_code is a function name, not a label. Give the client something better to put on the button, and describe each argument so the form explains itself:
server.py from typing import Annotated
from pydantic import Field
from mcp.server import MCPServer
mcp = MCPServer ( "Code Helper" )
@mcp . prompt ( title = "Code review" )
def review_code (
code : Annotated [ str , Field ( description = "The code to review." )],
language : Annotated [ str , Field ( description = "The language the code is written in." )] = "python" ,
) -> str :
"""Review a piece of code."""
return f "Please review this { language } code: \n\n { code } "
* title="Code review" is the human-readable name, exactly like a tool's title .
* Annotated[str, Field(description=...)] is the same pattern Tools uses to describe a tool's parameters. Here the description lands on the argument instead of in a schema.
* language has a default, so it stops being required.
The prompts/list entry now carries everything a client needs to draw a good form:
{
"name" : "review_code" ,
"title" : "Code review" ,
"description" : "Review a piece of code." ,
"arguments" : [
{ "name" : "code" , "description" : "The code to review." , "required" : true },
{ "name" : "language" , "description" : "The language the code is written in." , "required" : false }
]
}
Info
If you have read Tools , you already know everything up to this point. Same decorator, same
docstring-as-description, same Annotated / Field . The only things that change are who
triggers it (the user) and where the result goes (into the conversation).
More than text
UserMessage and AssistantMessage also accept a content block, or an Image / Audio helper, wherever they accept a str . Two cases come up in prompts: attaching a document and attaching a picture.
Embedding a file
server.py from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver import Message , UserMessage
from mcp.types import EmbeddedResource , TextResourceContents
mcp = MCPServer ( "Code Helper" )
STYLE_GUIDE_FILE = Path ( __file__ ) . parent / "style-guide.md" # or the path to your file on disk
@mcp . resource ( "style://python" , mime_type = "text/markdown" )
def style_guide () -> str :
"""The team's Python style guide."""
return STYLE_GUIDE_FILE . read_text ( encoding = "utf-8" )
@mcp . prompt ()
def review_code ( code : str ) -> list [ Message ]:
"""Review a piece of code against the team style guide."""
guide = TextResourceContents ( uri = "style://python" , mime_type = "text/markdown" , text = style_guide ())
return [
UserMessage ( EmbeddedResource ( resource = guide )),
UserMessage ( f "Review this code against the style guide above: \n\n { code } " ),
]
* The style guide is a resource at style://python ( Resources covers those), read from a style-guide.md next to server.py . Put any Markdown file there.
* EmbeddedResource(resource=TextResourceContents(...)) , both from mcp.types , carries the file with its URI and MIME type as the first message; the request that refers to it follows as plain text.
* Embedding, rather than pasting the guide into the f-string, lets the client show it as an attachment and reopen style://python later, and the model receives the file verbatim. For a binary file use BlobResourceContents with a base64 blob .
Rendered, the first message's content is a resource block:
{ "type" : "resource" , "resource" : { "uri" : "style://python" , "mimeType" : "text/markdown" , "text" : "* Prefer early returns.\n..." }}
Attaching an image
server.py from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver import Image , Message , UserMessage
mcp = MCPServer ( "Code Helper" )
DIAGRAM_FILE = Path ( __file__ ) . parent / "architecture.png" # or the path to your file on disk
@mcp . prompt ()
def explain_component ( component : str ) -> list [ Message ]:
"""Explain one component using the architecture diagram."""
return [
UserMessage ( Image ( path = DIAGRAM_FILE )),
UserMessage ( f "Where does { component } sit in this architecture, and what does it talk to?" ),
]
* Image is the helper from Images, audio & icons . UserMessage converts it to an ImageContent block (the file base64-encoded, MIME type guessed from .png ) when the prompt renders; Audio becomes an AudioContent the same way.
* Put any PNG named architecture.png beside server.py . Prompt arguments are strings, so the picture always comes from the server; component only supplies the words.
{ "type" : "image" , "data" : "iVBORw0KGgoAAAANSUhEUg..." , "mimeType" : "image/png" }
Changing the list at runtime
Prompts can be added while clients are connected, for example to let a user save an instruction as a menu entry of their own. Register the prompt, then notify:
server.py from contextlib import suppress
from mcp.server import MCPServer
from mcp.server.mcpserver import Context
from mcp.server.mcpserver.prompts import Prompt
mcp = MCPServer ( "Code Helper" )
@mcp . prompt ()
def review_code ( code : str ) -> str :
"""Review a piece of code."""
return f "Please review this code: \n\n { code } "
@mcp . tool ()
async def save_template ( name : str , instruction : str , ctx : Context ) -> str :
"""Save an instruction as a prompt the user can pick from the menu."""
def template ( code : str ) -> str :
return f " { instruction } \n\n { code } "
with suppress ( ValueError ): # replace an existing entry of the same name
mcp . remove_prompt ( name )
mcp . add_prompt ( Prompt . from_function ( template , name = name , description = instruction ))
await ctx . notify_prompts_changed ()
await ctx . session . send_prompt_list_changed ()
return f "Saved ' { name } ' to the prompt menu."
* mcp.add_prompt(Prompt.from_function(fn, name=..., description=...)) registers a function exactly as @mcp.prompt() would, and mcp.remove_prompt(name) is the reverse. add_prompt keeps an existing entry of the same name rather than overwrite it, so the tool removes any old one first to make saving a replace. prompts/list reflects the change immediately.
* await ctx.notify_prompts_changed() sends notifications/prompts/list_changed to every 2026-07-28 client listening on a subscriptions/listen stream ( Subscriptions ). await ctx.session.send_prompt_list_changed() sends it to the calling client when that client is pre-2026 ( Serving legacy clients ). Call both; each does nothing when there is nobody to tell.
* A client that receives the notification calls prompts/list again. In the Python Client that is async with client.listen(prompts_list_changed=True) as sub: , which yields a PromptsListChanged event.
Recap
* @mcp.prompt() on a function makes it a prompt. Name from the function, description from the docstring.
* Prompts are user-controlled : the client lists them, the user picks one and fills in the arguments.
* Arguments are a flat list of named strings (no schema). A parameter with a default is optional.
* Return a str and it becomes one user message. Return a list of UserMessage / AssistantMessage to seed a multi-turn conversation.
* title= and Field(description=...) are what a client puts in its UI.
* A missing required argument fails the whole request. There is no per-prompt error result.
* Wrap an EmbeddedResource or an Image in a UserMessage to attach a document or a picture.
* Add or remove prompts at runtime with mcp.add_prompt(...) / mcp.remove_prompt(...) , then await ctx.notify_prompts_changed() and await ctx.session.send_prompt_list_changed() .
Server-side autocomplete for a prompt's (or a resource template's) arguments is Completions .
Back to top
Previous
URI templates
Next
Completions
Made with
Zensical
Links found on this page
- Skip to content [direct]
- en - English [direct]
- de - Deutsch [direct]
- es - español [direct]
- fr - français [direct]
- hi - हिन्दी [direct]
- ja - 日本語 [direct]
- ko - 한국어 [direct]
- pt - português (Brasil) [direct]
- ru - русский язык [direct]
- tr - Türkçe [direct]
- uk - українська мова [direct]
- zh - 简体中文 [direct]
- zh-hant - 繁體中文 [direct]
- modelcontextprotocol/python-sdk [direct]
- What's new in v2 [direct]
- Get started [direct]
- Installation [direct]
- First steps [direct]
- Connect to a real host [direct]
- Testing [direct]
- Servers [direct]
- Tools [direct]
- Structured Output [direct]
- Resources [direct]
- URI templates [direct]
- Completions [direct]
- Images, audio & icons [direct]
- Handling errors [direct]
- Inside your handler [direct]
- The Context [direct]
- Dependencies [direct]
- Lifespan [direct]
- Elicitation [direct]
- Multi-round-trip requests [direct]
- Sampling and roots [direct]
- Progress [direct]
- Logging [direct]
- Subscriptions [direct]
- Running your server [direct]
- Add to an existing app [direct]
- Deploy & scale [direct]
- Authorization [direct]
- OpenTelemetry [direct]
- Serving legacy clients [direct]
- Clients [direct]
- Callbacks [direct]
- Transports [direct]
- OAuth [direct]
- Identity assertion [direct]
- Multiple servers [direct]
- Subscriptions [direct]
- Caching [direct]
- Protocol versions [direct]
- Deprecated features [direct]
- Advanced [direct]
- The low-level Server [direct]
- Pagination [direct]
- Middleware [direct]
- Extensions [direct]
- MCP Apps [direct]
- Troubleshooting [direct]
- Translations [direct]
- Migration Guide [direct]
- mcp [direct]
- mcp_types [direct]
- Zensical [direct]