Chapter 5: Reusable Chat Starters - FastMCP Prompts (Prompt
, PromptManager
)
In Chapter 4: FastMCP Tools (Tool
, ToolManager
), we learned how to give our server specific actions it can perform, like a calculator tool. But modern AI often involves conversations, especially with Large Language Models (LLMs). How do we manage the instructions and conversation starters we send to these models?
Imagine you want to build an AI assistant tool that can summarize text. You’ll need to tell the underlying LLM what to do (summarize) and what text to summarize. You might also want to provide specific instructions like “Keep the summary under 50 words.” You’ll probably need variations of this prompt for different tasks. Writing this message structure over and over again in your tool code would be repetitive and hard to manage.
This is where FastMCP Prompts come in. They provide a way to create reusable templates for generating sequences of messages, perfect for starting conversations with LLMs or structuring requests.
The Mad Libs Analogy: Prompts and the Prompt Manager
Think of a Prompt
like a Mad Libs story template. A Mad Libs template has a pre-written story with blanks (like ___(noun)___
or ___(verb)___
). You define the structure and the blanks.
Prompt
: The Mad Libs template itself. It has a name (like “Vacation Story”) and defined blanks. In FastMCP, the “story” is a sequence of messages (usually for an LLM), and the blanks arePromptArgument
objects.PromptArgument
: Represents a blank in the template. It defines the name of the blank (e.g.,text_to_summarize
), maybe a description, and whether it’s required.- Rendering: The act of filling in the blanks. You provide values (arguments) for the blanks (
text_to_summarize = "Once upon a time..."
), and the template generates the complete story. In FastMCP, rendering aPrompt
with arguments produces a list ofPromptMessage
objects (likeUserMessage
orAssistantMessage
). These messages have roles (user
,assistant
) and content, ready to be sent to an LLM. PromptManager
: Like a folder or binder holding all your different Mad Libs templates. It’s the part ofFastMCP
that stores and helps you find and use (render
) your definedPrompt
templates.
Clients (like an AI application) can ask the PromptManager
(via FastMCP
) to list available prompt templates (listPrompts
) and then request a specific, filled-in prompt sequence using its name and arguments (getPrompt
).
Creating Your First Prompt Template: Using @server.prompt()
Just like @server.tool()
and @server.resource()
, FastMCP
provides a simple decorator, @server.prompt()
, to easily define these message templates using Python functions.
Let’s create a prompt template for our text summarization task.
File: summarizer_server.py
# 1. Import FastMCP and message types
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import UserMessage # We'll use this
# 2. Create the server instance
server = FastMCP(name="SummarizerServer")
# 3. Use the @server.prompt() decorator to define our template
@server.prompt(name="summarize_text", description="Generates messages to ask an LLM to summarize text.")
def create_summary_prompt(text_to_summarize: str) -> list[UserMessage]:
"""
This function defines the 'summarize_text' prompt template.
'text_to_summarize: str' defines a required argument (a blank).
'-> list[UserMessage]' indicates it returns a list of messages.
"""
print(f"Rendering prompt 'summarize_text' with text: {text_to_summarize[:30]}...") # Log
# 4. Construct the message(s) based on the arguments
# Here, we create a single user message containing instructions and the text.
prompt_content = f"Please summarize the following text concisely:\n\n{text_to_summarize}"
# Return a list containing one UserMessage object
return [UserMessage(content=prompt_content)]
# 5. Standard run block (optional: add a tool that uses this prompt later)
if __name__ == "__main__":
print(f"Starting {server.name}...")
server.run()
print(f"{server.name} finished.")
Explanation:
- Imports: We import
FastMCP
andUserMessage
(a specific type ofPromptMessage
).AssistantMessage
is also available. server = FastMCP(...)
: Creates our server. Internally, this also creates aPromptManager
.@server.prompt(...)
: This decorator registers our function as a prompt template.name="summarize_text"
: The name clients will use to request this template.description="..."
: A helpful description.
def create_summary_prompt(...)
: This Python function builds the message list when the prompt is rendered.text_to_summarize: str
: The type hint defines a requiredPromptArgument
namedtext_to_summarize
. This is the blank in our Mad Libs.-> list[UserMessage]
: The type hint tellsFastMCP
that this function will return a list containingUserMessage
objects (or compatible types like plain strings or dicts that look like messages).- The function body uses the input argument (
text_to_summarize
) to construct the desired message content. - It returns a list containing a single
UserMessage
. You could return multiple messages (e.g., alternating user/assistant roles) to set up a conversation history.
server.run()
: Starts the server. ThePromptManager
now knows about thesummarize_text
prompt template.
What happens when a client uses this prompt?
- Discovery (Optional): A client might call
listPrompts
. The server (usingPromptManager
) would respond with information about thesummarize_text
prompt, including its name, description, and the required argumenttext_to_summarize
(string). - Rendering Request: The client wants to generate the messages for summarizing a specific text. It sends an MCP request:
getPrompt
withname="summarize_text"
andarguments={"text_to_summarize": "This is the text..."}
. - Server-Side Rendering:
FastMCP
receives the request and asks itsPromptManager
to render the prompt.PromptManager
finds thePrompt
object associated withsummarize_text
.- It calls the
render
method on thePrompt
object, which in turn calls your Python functioncreate_summary_prompt(text_to_summarize="This is the text...")
. - Your function runs, builds the
prompt_content
string, and returns[UserMessage(content="Please summarize...")]
. FastMCP
takes this list ofMessage
objects.
- Response:
FastMCP
sends the generated message list back to the client in thegetPrompt
response. The client now has the structured message(s) ready to be sent to an LLM.
// Example Client Request (Simplified MCP format)
{
"method": "getPrompt",
"params": {
"name": "summarize_text",
"arguments": {
"text_to_summarize": "The quick brown fox jumps over the lazy dog."
}
}
}
// Example Server Response (Simplified MCP format)
{
"result": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please summarize the following text concisely:\n\nThe quick brown fox jumps over the lazy dog."
}
}
]
}
}
This makes it easy for client applications to get consistently formatted prompts for various tasks without needing to know the exact text structure themselves.
Returning Different Message Types
Your prompt function can return various things, and FastMCP
will try to convert them into the standard Message
format (like UserMessage
or AssistantMessage
):
- A single string: Automatically converted to
UserMessage(content=TextContent(type="text", text=your_string))
. - A
Message
object (e.g.,UserMessage
,AssistantMessage
): Used directly. - A dictionary matching the
Message
structure: e.g.,{"role": "user", "content": "Hello!"}
. Validated and converted. - A list containing any mix of the above: Each item is converted/validated.
from mcp.server.fastmcp import FastMCP
# Import both message types
from mcp.server.fastmcp.prompts import UserMessage, AssistantMessage
server = FastMCP(name="MultiMessageServer")
@server.prompt(name="greet_user", description="Starts a simple conversation.")
def greeting_prompt(user_name: str): # -> returns list of mixed types
"""Generates a multi-turn conversation starter."""
# We can return a list containing different types:
return [
# A UserMessage object
UserMessage(f"Hello {user_name}, tell me about your day."),
# A dictionary that looks like an AssistantMessage
{"role": "assistant", "content": "I'm ready to listen!"},
# A simple string (becomes a UserMessage)
"Start whenever you're ready.",
]
# ... (run block) ...
This flexibility lets you structure complex conversational prompts easily.
How Prompts Work Under the Hood
Using @server.prompt()
is straightforward, but what’s happening inside FastMCP
and its PromptManager
?
1. Registration (When the server code loads):
- Python executes your
summarizer_server.py
. - It reaches the
@server.prompt(name="summarize_text", ...)
line abovedef create_summary_prompt(...)
. - This calls the
server.prompt()
method (inserver/fastmcp/server.py
). This method returns a decorator function that is immediately applied tocreate_summary_prompt
. - The decorator function calls
server.add_prompt()
. server.add_prompt()
callsself._prompt_manager.add_prompt()
.- Inside
PromptManager.add_prompt
(inserver/fastmcp/prompts/manager.py
):- It calls
Prompt.from_function(create_summary_prompt, name="summarize_text", ...)
(seeserver/fastmcp/prompts/base.py
). Prompt.from_function
inspects thecreate_summary_prompt
function:- Gets its name (
summarize_text
). - Gets its description (from decorator or docstring).
- Looks at the parameters (
text_to_summarize: str
) using Python’s introspection to determine the requiredPromptArgument
s. - Creates a
Prompt
object storing the function itself (fn
), its name, description, and the list of arguments.
- Gets its name (
- The
PromptManager
stores thisPrompt
object in its internal dictionary, keyed by the name"summarize_text"
.
- It calls
2. Rendering (When a client calls getPrompt
):
- A client sends the MCP
getPrompt
request we saw earlier. FastMCP
receives this and calls its internalget_prompt
handler method (defined inserver/fastmcp/server.py
).- This handler calls
self._prompt_manager.render_prompt("summarize_text", {"text_to_summarize": "..."})
. - Inside
PromptManager.render_prompt
:- It looks up
"summarize_text"
in its dictionary and finds the correspondingPrompt
object. - It calls the
Prompt
object’srender
method:prompt.render(arguments={"text_to_summarize": "..."})
.
- It looks up
- Inside
Prompt.render
(inserver/fastmcp/prompts/base.py
):- It validates that all required arguments (like
text_to_summarize
) were provided. - It calls the original Python function stored in
prompt.fn
:create_summary_prompt(text_to_summarize="...")
. - Your function executes and returns the list
[UserMessage(...)]
. - The
render
method takes this result, validates that each item is (or can be converted to) aMessage
object, and ensures the final output is a list ofMessage
s.
- It validates that all required arguments (like
- The
PromptManager
receives this validated list ofMessage
objects. FastMCP
takes the result, packages it into the standard MCPGetPromptResult
format (which contains themessages
list), and sends it back to the client.
Simplified Sequence Diagram (getPrompt
for summarize_text
):
sequenceDiagram
participant Client
participant FastMCP_Server as FastMCP (server.py)
participant PromptMgr as PromptManager (_prompt_manager)
participant SummaryPrompt as Prompt (wraps create_summary_prompt)
participant PromptFunc as create_summary_prompt()
Client->>+FastMCP_Server: Send MCP Request: getPrompt(name="summarize_text", args={"text": "..."})
FastMCP_Server->>+PromptMgr: render_prompt(name="summarize_text", args={...})
PromptMgr->>PromptMgr: Find Prompt object for "summarize_text"
PromptMgr->>+SummaryPrompt: prompt.render(arguments={...})
SummaryPrompt->>+PromptFunc: Call create_summary_prompt(text_to_summarize="...")
PromptFunc-->>-SummaryPrompt: Return [UserMessage(content="Summarize: ...")]
SummaryPrompt->>SummaryPrompt: Validate & format message list
SummaryPrompt-->>-PromptMgr: Return validated [UserMessage(...)]
PromptMgr-->>-FastMCP_Server: Return [UserMessage(...)]
FastMCP_Server->>-Client: Send MCP Response: result={messages: [{...}]}
Looking at the Code (Briefly):
You don’t need to memorize the internal details, but seeing where things happen can clarify the process:
- Registration (
@server.prompt
->add_prompt
->PromptManager.add_prompt
):server.py
:FastMCP.prompt
decorator callsself.add_prompt
.server.py
:FastMCP.add_prompt
callsself._prompt_manager.add_prompt
.manager.py
:PromptManager.add_prompt
callsPrompt.from_function
and stores the result.
# Inside server/fastmcp/prompts/manager.py (Simplified PromptManager.add_prompt) from .base import Prompt class PromptManager: # ... (init, get_prompt, list_prompts) ... def add_prompt(self, prompt: Prompt) -> Prompt: # Check for duplicates... if prompt.name in self._prompts: # ... handle duplicate ... pass # Store the Prompt object self._prompts[prompt.name] = prompt return prompt # Note: Prompt.from_function (in base.py) does the function inspection.
- Rendering (
FastMCP.get_prompt
->PromptManager.render_prompt
->Prompt.render
):server.py
:FastMCP.get_prompt
handles incoming requests and callsself._prompt_manager.render_prompt
.
# Inside server/fastmcp/prompts/manager.py (Simplified PromptManager.render_prompt) class PromptManager: # ... (other methods) ... async def render_prompt(self, name, arguments=None): # 1. Find the prompt object by name prompt = self.get_prompt(name) if not prompt: raise ValueError(f"Unknown prompt: {name}") # 2. Tell the Prompt object to render itself return await prompt.render(arguments)
base.py
:Prompt.render
validates arguments and calls the stored function (self.fn
). It then processes the function’s return value into a list ofMessage
objects.
# Inside server/fastmcp/prompts/base.py (Simplified Prompt.render) class Prompt: # ... (init, from_function, PromptArgument) ... async def render(self, arguments=None): # Validate required arguments... # ... try: # Call the original decorated function result = self.fn(**(arguments or {})) if inspect.iscoroutine(result): # Handle async functions result = await result # Convert result to list of Message objects # (Handles strings, dicts, Message objects, lists) messages: list[Message] = [] # ... (conversion logic using message_validator) ... return messages except Exception as e: raise ValueError(f"Error rendering prompt {self.name}: {e}")
Conclusion
You’ve learned about FastMCP Prompts, a powerful way to manage reusable message templates, especially useful for interacting with language models.
- Prompts (
Prompt
) are like Mad Libs templates for creating sequences ofUserMessage
s andAssistantMessage
s. - They use
PromptArgument
s to define the “blanks” that need filling. - The
PromptManager
keeps track of all defined prompts. - The
@server.prompt()
decorator provides an easy way to define a prompt template using a Python function. The function’s parameters become arguments, and its return value (string, dict, Message object, or list thereof) defines the generated message sequence. - Clients use
listPrompts
to discover templates andgetPrompt
to render a specific template with arguments, receiving the generated messages back.
Prompts help keep your LLM interaction logic organized, reusable, and separate from your main tool code.
In the next chapter, we’ll explore a concept that ties tools, resources, and potentially prompts together during a request: Chapter 6: FastMCP Context (Context
). This allows your tools and resources to access server capabilities like logging and progress reporting.
Generated by AI Codebase Knowledge Builder