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 are PromptArgument 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 a Prompt with arguments produces a list of PromptMessage objects (like UserMessage or AssistantMessage). 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 of FastMCP that stores and helps you find and use (render) your defined Prompt 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:

  1. Imports: We import FastMCP and UserMessage (a specific type of PromptMessage). AssistantMessage is also available.
  2. server = FastMCP(...): Creates our server. Internally, this also creates a PromptManager.
  3. @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.
  4. 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 required PromptArgument named text_to_summarize. This is the blank in our Mad Libs.
    • -> list[UserMessage]: The type hint tells FastMCP that this function will return a list containing UserMessage 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.
  5. server.run(): Starts the server. The PromptManager now knows about the summarize_text prompt template.

What happens when a client uses this prompt?

  1. Discovery (Optional): A client might call listPrompts. The server (using PromptManager) would respond with information about the summarize_text prompt, including its name, description, and the required argument text_to_summarize (string).
  2. Rendering Request: The client wants to generate the messages for summarizing a specific text. It sends an MCP request: getPrompt with name="summarize_text" and arguments={"text_to_summarize": "This is the text..."}.
  3. Server-Side Rendering:
    • FastMCP receives the request and asks its PromptManager to render the prompt.
    • PromptManager finds the Prompt object associated with summarize_text.
    • It calls the render method on the Prompt object, which in turn calls your Python function create_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 of Message objects.
  4. Response: FastMCP sends the generated message list back to the client in the getPrompt 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 above def create_summary_prompt(...).
  • This calls the server.prompt() method (in server/fastmcp/server.py). This method returns a decorator function that is immediately applied to create_summary_prompt.
  • The decorator function calls server.add_prompt().
  • server.add_prompt() calls self._prompt_manager.add_prompt().
  • Inside PromptManager.add_prompt (in server/fastmcp/prompts/manager.py):
    • It calls Prompt.from_function(create_summary_prompt, name="summarize_text", ...) (see server/fastmcp/prompts/base.py).
    • Prompt.from_function inspects the create_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 required PromptArguments.
      • Creates a Prompt object storing the function itself (fn), its name, description, and the list of arguments.
    • The PromptManager stores this Prompt object in its internal dictionary, keyed by the name "summarize_text".

2. Rendering (When a client calls getPrompt):

  • A client sends the MCP getPrompt request we saw earlier.
  • FastMCP receives this and calls its internal get_prompt handler method (defined in server/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 corresponding Prompt object.
    • It calls the Prompt object’s render method: prompt.render(arguments={"text_to_summarize": "..."}).
  • Inside Prompt.render (in server/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) a Message object, and ensures the final output is a list of Messages.
  • The PromptManager receives this validated list of Message objects.
  • FastMCP takes the result, packages it into the standard MCP GetPromptResult format (which contains the messages 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 calls self.add_prompt.
    • server.py: FastMCP.add_prompt calls self._prompt_manager.add_prompt.
    • manager.py: PromptManager.add_prompt calls Prompt.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 calls self._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 of Message 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 of UserMessages and AssistantMessages.
  • They use PromptArguments 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 and getPrompt 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