Tools, Resources, and Prompts

Prompt Templates in MCP

4 min read

Prompts are the least-used of the three capabilities, and the one that most often should have been used. They are conversation starters your server hands to the user, not to the model — which makes them the right home for the instructions your team keeps retyping slightly differently every time.

Why Prompts?

The honest case for them: somewhere in your organisation there is a paragraph of instructions that one person writes well and everyone else writes badly. A prompt template is where that paragraph goes to stop being tribal knowledge.

Prompts are useful for:

  • Standardizing interactions that are currently copy-pasted from a wiki
  • Giving domain-specific starting points to people who don't know how to ask
  • Encapsulating instructions too long for anyone to retype accurately

Anatomy of a prompt definition

Four parts, and the split between definition and generation is the part people get wrong first:

What a Prompt definition is made of

  1. 01 · name

    The stable identifier the host calls. Treat it as an API surface — renaming it breaks saved workflows

    code_review
  2. 02 · description

    What the user sees in the host's prompt picker. Written for a human choosing from a menu, not for the model

    Review code for best practices and issues
  3. 03 · arguments

    The blanks the user fills in. Mark only what you genuinely cannot default as required

    language (required) · focus (optional)
  4. 04 · messages

    Generated on demand by get_prompt, with the arguments interpolated. This is the part the model actually reads

    role: user → "Review the following {language} code…"

Click any slot to reveal an example.

The first three are declared once in list_prompts — they are a menu entry. The fourth is built fresh in get_prompt every time someone picks it. Keeping that separation clear is why a prompt can be cheap to list and expensive to build.

Defining Prompts

@mcp.prompt()
def code_review(language: str, focus: str = "general best practices") -> str:
    """Review code for best practices and issues."""
    return f"""Please review the following {language} code.
Focus on: {focus}

Provide specific suggestions with line numbers where applicable.
Rate the code from 1-10 and explain your rating."""

Declaring the prompt and building it are the same function. The parameters become the prompt's arguments, and a parameter with a default is reported to the client as optional — so required is derived from your signature rather than restated next to it.

Generating Prompt Content

When a prompt is requested, return the actual messages:

@mcp.prompt()
def summarize_pr(title: str, diff: str) -> str:
    """Summarise a pull request for a reviewer who has not read the diff."""
    return f"""Summarise this pull request for someone reviewing it cold.

Title: {title}

{diff}

Lead with the behaviour change, not the file list. Call out anything that
touches auth, migrations, or public API shape."""

Returning a plain string is the common case: the SDK wraps it as a single user message.

Multi-Turn Prompts

Prompts can include multiple messages for complex workflows:

from mcp.server.mcpserver.prompts.base import UserMessage, AssistantMessage


@mcp.prompt()
def debug_session(error: str, code: str) -> list:
    """Open a step-by-step debugging conversation around one error."""
    return [
        UserMessage("I'm going to share an error. Help me debug it step by step."),
        AssistantMessage("I'll help you debug. Please share the error message and relevant code."),
        UserMessage(f"Error: {error}\nCode: {code}"),
    ]

Return a list of messages when the prompt needs to pre-load a conversation rather than ask a single question. The seeded assistant turn is the point: it commits the model to a stance before the user's real input arrives.

Use UserMessage / AssistantMessage from mcpserver.prompts.base, not mcp.types.PromptMessage. They look interchangeable and they are not: a PromptMessage falls through the prompt manager's conversion to a fallback branch that stamps every message role="user". Nothing errors — you simply get three user turns and the assistant turn you wrote is silently gone, which is the whole reason you used a message list.

Dynamic Prompt Arguments

Fetch argument options dynamically:

Prompt(
    name="query_database",
    description="Query a specific table",
    arguments=[
        PromptArgument(
            name="table",
            description="Table name",
            required=True,
            # Could be validated against actual schema
        )
    ]
)

Next: one server that offers all three capabilities without them tripping over each other. :::

Quiz

Module 3 Quiz: Tools, Resources, and Prompts

Take Quiz
Was this lesson helpful?

Sign in to rate