MCP Prompts Explained: Reusable Prompt Templates as a Protocol Feature
Why "just write a good prompt" stops working at scale
Every team that builds with LLMs eventually hits the same wall. Someone writes a really good prompt for summarizing support tickets, or generating a code review checklist, or drafting a release note from a git diff. It works great. Then it gets copy-pasted into three different tools, a teammate tweaks a line and forgets to share the update, and six months later nobody knows which version of "the good prompt" is actually running in production. Prompt logic that lives in application code — string templates buried in a prompts.py file, or worse, hardcoded inline in a chat handler — has no versioning story, no discovery story, and no reuse story across clients.
The Model Context Protocol (MCP) has an answer for this that gets far less attention than Tools, but is arguably just as important for building serious AI systems: Prompts. MCP Prompts are a first-class primitive that let a server expose reusable, parameterized prompt templates — not as documentation, not as a config file you have to know exists, but as something a client can discover, list, fill in, and execute through the protocol itself.
If you've spent time with MCP you already know about Tools (functions the model can call) and Resources (data the model can read). Prompts are the third leg of that stool, and this article is about what they are, why they exist as a distinct primitive instead of just being another tool, and how to actually build and use them.
The three MCP primitives, and where Prompts fit
MCP defines three ways a server can expose capabilities to a client:
- Resources — structured or unstructured data the client can read (files, database rows, API responses). The client decides when to pull these in.
- Tools — functions the *model* decides to invoke during a conversation, based on its own reasoning about what it needs.
- Prompts — templated messages or workflows that the *user* (or the client application) explicitly selects to kick off an interaction.
That last distinction is the key one. Tools are model-invoked: the LLM looks at the conversation, decides it needs to call search_orders, and does so autonomously. Prompts are user-invoked: a human (or a UI acting on their behalf) picks a prompt from a menu, fills in some arguments, and that becomes the starting message — or sequence of messages — sent to the model.
Think of Prompts as the MCP equivalent of a slash command or a saved snippet in your editor, except the definition lives on the server, not scattered across whatever client happens to be open. A code review server might expose a review_pr prompt. A support-ticket server might expose draft_response and escalate_summary. A documentation server might expose explain_concept. In each case, the server owns the prompt engineering, and every client that connects — Claude Desktop, a custom agent, an IDE extension — gets access to the same, tested template.
Anatomy of an MCP Prompt
Structurally, a prompt definition in MCP has a few required pieces:
- name — a unique identifier, like
summarize_threadorgenerate_test_cases. - description — human-readable text explaining what the prompt does, shown in client UIs so users can pick the right one.
- arguments — an optional list of named parameters the prompt accepts, each with its own name, description, and whether it's required.
When a client wants to use a prompt, the interaction follows two protocol methods:
prompts/list— the client asks the server "what prompts do you have?" and gets back the catalog of names, descriptions, and argument schemas.prompts/get— the client asks for a specific prompt by name, passing in argument values, and the server returns the fully rendered message (or messages) ready to send to the model.
This is the crucial part: the server doesn't just hand back a string with placeholders. It renders the template server-side and returns actual PromptMessage objects — the same message format used elsewhere in MCP, with roles (user, assistant) and content blocks (text, images, embedded resources). That means a prompt can return a multi-turn conversation seed, not just a single string, and it can pull in live data (like the actual contents of a file or a resource) at render time.
A minimal Prompt server in Python
Here's a small but complete example using the official MCP Python SDK, exposing a single prompt for turning a bug description into a structured issue report.
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
mcp = FastMCP("issue-writer")
@mcp.prompt()
def bug_report(component: str, symptom: str, severity: str = "medium") -> str:
"""Generate a structured bug report template for triage."""
return f"""You are a QA lead writing a bug report for the '{component}' component.
Symptom reported: {symptom}
Severity: {severity}
Write a structured bug report with these sections:
- Summary (one line)
- Steps to Reproduce
- Expected Result
- Actual Result
- Suggested Severity Justification
Keep it factual. Do not invent steps you were not given."""
@mcp.prompt()
def code_review(diff: str, focus_area: str = "correctness") -> list[base.Message]:
"""Multi-turn prompt seeding a focused code review conversation."""
return [
base.UserMessage(
f"Review this diff with special attention to {focus_area}:\n\n{diff}"
),
base.AssistantMessage(
"Understood. I'll review for "
f"{focus_area} issues first, then note anything else "
"significant. Ready when you share the diff."
),
]
if __name__ == "__main__":
mcp.run()A few things worth noticing here. The bug_report function returns a plain string, which the SDK automatically wraps into a single user message. The code_review function returns a list of base.Message objects directly, which lets you seed a conversation with both a user turn and a pre-written assistant acknowledgment — useful when you want to steer the model's opening stance before the real exchange begins. Both functions are decorated with @mcp.prompt(), and the SDK automatically derives the argument schema from the Python function signature: component, symptom, and severity become the prompt's declared arguments, with severity marked optional because it has a default value.
This is the pattern worth internalizing: prompt arguments are just function parameters. You don't write a separate schema by hand in most SDKs — you write a typed function, and the protocol plumbing (the prompts/list schema, the prompts/get rendering) falls out of that automatically.
Why not just make it a Tool?
This is the question every team asks the first time they meet MCP Prompts, so it's worth answering directly. You *could* implement bug_report as a tool instead — the model could call a generate_bug_report function and get a string back. But that changes who is in control and when the interaction happens.
- Tools are model-initiated. The LLM decides, mid-conversation, based on its own judgment, that it should call a tool. You don't control when that happens.
- Prompts are user-initiated. A person (or a UI layer) explicitly picks the prompt from a list, before the model does any reasoning at all. It's the entry point to a conversation, not a step within one.
This matters most in client UIs. A well-behaved MCP client — Claude Desktop is a good example — surfaces prompts as slash commands or a picker menu. A user can type /bug_report and get a form asking for component, symptom, and severity, without the model ever needing to guess what the user wants. That's a fundamentally different UX than hoping the model infers "oh, they want a bug report" from a vague message.
There's a reliability angle here too. Tool calls depend on the model correctly reasoning that a tool is relevant, correctly extracting arguments from freeform conversation, and correctly formatting the call. That's usually fine, but it's still inference, and inference can misfire — the model might not realize a tool exists for the task, or might call it with the wrong arguments. A Prompt sidesteps all of that: the human explicitly selects bug_report and explicitly types component=checkout-flow, so there's no ambiguity for the model to get wrong. For workflows where you need deterministic, repeatable framing — onboarding flows, compliance-sensitive report generation, anything you'd otherwise put in a runbook — that determinism is worth a lot more than it sounds.
There's also a workflow-design reason. Prompts are meant to encode *known-good starting points* for common tasks — the equivalent of a team's prompt-engineering playbook, versioned and shipped with the server instead of pasted into a wiki page. Tools are meant to encode *capabilities* the model can reach for autonomously. Conflating the two means your best prompt engineering either gets buried inside tool-calling logic the model might not trigger, or gets duplicated by hand across every client that wants to use it.
Prompts that pull in live Resources
The real power of MCP Prompts shows up when they combine with Resources. A prompt isn't limited to interpolating strings you pass in as arguments — the server can reach into its own data layer at render time and embed real content into the returned messages.
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.prompts import base
mcp = FastMCP("release-notes")
@mcp.prompt()
async def release_summary(version: str) -> list[base.Message]:
"""Draft release notes from the changelog for a given version."""
# Server-side lookup — the client never sees this step,
# it just gets the fully rendered prompt back.
changelog_text = await fetch_changelog_section(version)
return [
base.UserMessage(
f"Here is the raw changelog for version {version}:\n\n"
f"{changelog_text}\n\n"
"Turn this into user-facing release notes. Group changes "
"into 'New Features', 'Fixes', and 'Breaking Changes'. "
"Use plain language, no internal ticket numbers."
)
]
async def fetch_changelog_section(version: str) -> str:
# In a real server this would query a database, hit a git log,
# or read from a CHANGELOG.md resource already exposed by this server.
return f"- Fixed timeout bug in export job (v{version})\n- Added dark mode"Notice that the client only ever sees the *rendered* result of prompts/get — it doesn't need to know the server went and fetched a changelog section behind the scenes. This is exactly the same trust boundary that makes Tools useful: the server can hide arbitrarily complex logic (database queries, API calls, file reads) behind a clean, declarative interface, and the client just gets back messages ready to send to the model.
This is also where Prompts and Resources genuinely complement each other rather than duplicating functionality. A Resource says "here is a piece of data you can read." A Prompt says "here is a piece of data, already embedded into a well-crafted instruction, ready to kick off a specific task." You could hand a client the raw changelog resource and let it figure out what to do with it — but shipping the release_summary prompt guarantees every team member gets the same framing, the same section headers, the same tone instructions, every time.
Discovery, listing, and the client-side experience
From the client's perspective, working with prompts is a two-step dance, and it's worth walking through what actually crosses the wire.
- On connecting to a server, the client calls
prompts/list. It gets back something structurally like:
{
"prompts": [
{
"name": "bug_report",
"description": "Generate a structured bug report template for triage.",
"arguments": [
{"name": "component", "required": true},
{"name": "symptom", "required": true},
{"name": "severity", "required": false}
]
},
{
"name": "code_review",
"description": "Multi-turn prompt seeding a focused code review conversation.",
"arguments": [
{"name": "diff", "required": true},
{"name": "focus_area", "required": false}
]
}
]
}- When a user selects
bug_reportand fills in values, the client callsprompts/getwithname: "bug_report"and the arguments. The server renders the template and returns the actual message list, ready to be dropped into the conversation and sent to the model.
Good MCP clients also support listChanged notifications for prompts — meaning if a server dynamically adds or removes prompts (say, a prompt catalog that's generated from a database of team-approved templates), it can tell connected clients to refresh their list without the user needing to reconnect. This matters more than it sounds: it means a prompt library can be a living, centrally-managed asset rather than something baked into a client release.
Designing good Prompt arguments (and the mistakes that keep recurring)
A few practical lessons show up quickly once you start building prompt servers for real workflows:
- Keep required arguments to the minimum that actually changes behavior. If
severityalmost always defaults sensibly, make it optional. Every required argument is a form field a human has to fill in before they can even start. - Write descriptions for humans, not for the model. The
descriptionfield on both the prompt itself and each argument gets rendered in client UIs — a picker menu, a command palette, a form. "Generate a structured bug report template for triage" is a UI label. Write it like one. - Prefer enums or constrained strings for arguments with a small set of valid values (like
severity: "low" | "medium" | "high"), and validate them server-side even if the client doesn't enforce it — not every client will build a dropdown, some will just accept free text. - Return multi-message prompts when you want to constrain the model's opening move, as in the
code_reviewexample. A pre-seeded assistant turn is a cheap, reliable way to lock in tone or approach before the real back-and-forth starts, and it's something a single-string prompt can't do. - Version by name, not by mutation. If you need to change a prompt's behavior in a way that breaks existing callers, ship it as
bug_report_v2rather than silently changing whatbug_reportdoes. Teams that depend on the old template shouldn't get surprised.
Alongside those design habits, a handful of mistakes recur often enough to call out specifically. Teams sometimes treat Prompts as documentation instead of executable templates — if your "prompt" is really just markdown instructions for a human to read and manually copy into a chat window, you're not using the protocol feature, you're using a wiki page. The point of prompts/get is that it returns something a client can send directly to a model, no copy-paste required. Teams also sometimes put model-invoked logic into a Prompt — if you find yourself wanting the model to decide, mid-task, "now I should fetch the release_summary prompt," that's a sign the functionality belongs in a Tool instead. Others skip argument validation because "the client will validate it," which breaks the moment a client passes through raw user text instead of rendering a strict form. And it's easy to hardcode secrets or environment-specific values into a prompt template instead of pulling them from server-side config, which quietly breaks portability across dev, staging, and prod. Finally, teams forget `listChanged` support when the prompt catalog is dynamic — if prompts are generated from a database table your team edits, a client that cached the list at startup will silently go stale.
Where Prompts sit in a production MCP architecture
Zooming out, MCP Prompts solve a specific and real organizational problem: prompt engineering ends up scattered and unversioned unless it has a proper home. Putting your best templates behind a Prompts interface on an MCP server gives you:
- A single source of truth. One rendering of
bug_report, used by every client — the Claude Desktop app, a Slack bot, a CI pipeline that opens tickets automatically. - Server-side control over quality. You can update the wording, add few-shot examples, or tune tone in one place, and every consumer picks it up on next connect.
- Clean separation from tool-calling logic. Your model's autonomous tool use isn't cluttered with "prompt templates" that were only ever meant to be user-selected starting points.
- A natural audit trail. Because prompt definitions live in server code (or a config the server loads), they go through the same code review and version control as everything else in your stack — no more hunting through Slack history for "the good version of the prompt."
None of this replaces good prompt engineering — a badly written bug_report template is still a badly written template whether it's stuffed inline in an app or exposed as a proper MCP Prompt. What the protocol gives you is distribution and consistency: write it once, expose it correctly, and every client that speaks MCP gets the same reliable starting point.
It's also worth being honest about the current maturity of this corner of the ecosystem. Tools get most of the attention in MCP tutorials and marketing material because "the model can call functions" is the flashier story. Prompts are quieter — they're infrastructure, not magic — which is exactly why they're easy to skip when you're prototyping and expensive to retrofit once three different applications have each grown their own copy of "the onboarding prompt" with slightly different wording. If you're building an MCP server that other teams or other applications will depend on, treat the Prompts capability as part of the initial design, not a nice-to-have you'll add later. Sketch out which user-initiated workflows in your domain deserve a named, versioned entry point, write them as typed functions the way the examples above do, and let the protocol handle discovery and rendering from there.
A practical starting checklist, if you're adding Prompts to an existing MCP server: list every place in your product where a user currently copies a hand-written prompt out of a doc or a Notion page; turn each one into a typed prompt function with explicit required and optional arguments; decide whether any of them should pull in live data via a Resource lookup at render time; and make sure your server emits listChanged if that catalog is going to grow. That's a small enough scope to ship in an afternoon, and it's the difference between "our prompts are tribal knowledge" and "our prompts are part of the API."
If you're building or extending MCP servers and want to go deeper — wiring up Prompts alongside Tools and Resources, handling listChanged notifications correctly, and structuring a server that a whole team can depend on — that's exactly the ground we cover in Building & Integrating MCP Servers, where we walk through production-grade server design end to end.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.