Prompts fill a gap tools and resources don't. They let the server author package expertise about how to ask for something, not just data or actions to ask for.
A prompt from prompts/list
{
"name": "summarize-pr",
"description": "Summarize a pull request's changes and flag potential risks.",
"arguments": [
{"name": "pr_number", "description": "Pull request number", "required": true},
{"name": "focus", "description": "Optional area to focus on, e.g. 'security'", "required": false}
]
}
Why this is a distinct primitive from tools
A tool called summarize_pr would let the model decide, mid-conversation, to summarize a PR. That's useful, but the exact instructions and framing baked into the request are whatever the model improvises from the tool's description. A prompt instead lets the server ship a carefully engineered template — one that instructs the model to check for missing tests, flag breaking API changes, and format output to match the team's review checklist, say. A user explicitly invokes that exact, curated interaction, instead of relying on the model to reconstruct it from a short tool description every time.
Where this shows up in practice
In a host with slash-command support, a user might type /summarize-pr 482. The host resolves that to a prompts/get call against the server that registered summarize-pr. It retrieves the fully rendered messages and injects them into the conversation as if the user had typed that much longer, more carefully worded prompt themselves.
The underlying idea
Prompts turn a server author's expertise about how to phrase a request into something reusable and shareable. No end user has to independently discover or write good prompts for a given task.
Related Resources
Prompt arguments look superficially similar to tool arguments, but the design intentionally diverges once you look closely.
Discovering arguments
// prompts/list result
{"prompts": [{
"name": "code-review",
"description": "Review a code diff against team standards.",
"arguments": [
{"name": "diff", "description": "The unified diff to review", "required": true},
{"name": "strictness", "description": "'lenient' or 'strict'", "required": false}
]
}]}
Invoking with arguments
{"method": "prompts/get", "params": {
"name": "code-review",
"arguments": {"diff": "diff --git a/app.py ...", "strictness": "strict"}
}}
Why simple strings, not JSON Schema
Tool inputSchema needs real typing — numbers, enums, nested objects — because a model fills it in while reasoning about structured data. Prompt arguments are designed to be filled in by a human, often typing directly into a slash-command-style text field, or through a simple host-rendered form. A flat string-keyed map with just a name, description, and required flag is enough for that. It keeps prompt definitions lightweight — you don't need a schema validator just to render a fill-in-the-blank template.
What the host is responsible for
Because argument metadata is intentionally light, hosts commonly render a basic form UI from the arguments array: one text field per argument, required ones marked. They collect values from the user and pass them straight through as strings in the prompts/get call. There's no expectation of client-side type coercion, only a check that required fields are present.
Related Resources
The prompts flow mirrors tools' list/invoke pattern. The result is a set of ready-to-use conversation messages, not a single result payload.
Discovery
{"method": "prompts/list"}
// -> {"prompts": [{"name": "code-review", "description": "...", "arguments": [...]}]}
Retrieval
{"method": "prompts/get", "params": {
"name": "code-review",
"arguments": {"diff": "diff --git a/app.py ...", "strictness": "strict"}
}}
// Result
{"result": {
"description": "A strict code review of the provided diff",
"messages": [
{"role": "user", "content": {"type": "text",
"text": "Review this diff strictly against our team's standards (null-safety, test coverage, no unused imports):\n\ndiff --git a/app.py ..."}}
]
}}
What the host does with the result
The host takes the messages array and injects it into the active conversation, as if the user had typed it. Often the user only saw a short slash command (/code-review strictness=strict) and a small form for the diff. The actual rendered prompt sent to the model is far more detailed and specific than what they typed.
Multi-message prompts
A GetPromptResult isn't limited to one message. A prompt can return several — a user message with instructions followed by an assistant message that seeds a partial response, for instance. This lets a server construct a small pre-scripted exchange, a common pattern for few-shot-style guidance, rather than a single freeform instruction.
Related Resources
Prompts and resources compose directly. A prompt's rendered messages can carry a resource's actual content inline, not just a reference to it.
Example: a prompt that embeds live data
{"result": {
"messages": [
{"role": "user", "content": {
"type": "resource",
"resource": {
"uri": "file:///project/CHANGELOG.md",
"mimeType": "text/markdown",
"text": "## v2.1.0\n- Fixed login bug\n- Added dark mode"
}
}},
{"role": "user", "content": {
"type": "text",
"text": "Summarize the above changelog for a non-technical release announcement."
}}
]
}}
Why embed rather than just mention the URI
If the prompt just said "read CHANGELOG.md and summarize it" as plain text, the model would need a separate tool call or resource read to get the content. That's an extra round trip, and it only works if the model happens to have a matching tool/resource-read capability available in that context. Embedding the resource's actual content directly into the returned message avoids that. The content is already present in the conversation the moment the prompt is invoked, with no dependency on the model fetching anything itself.
The tradeoff
Embedding content inflates the size of what prompts/get returns, and the content is a snapshot as of render time. If the underlying file changes a second later, the embedded copy in an already-rendered prompt won't reflect it, whereas a live resource read would. For small, relatively static reference material — a style guide, a fixed schema, a short changelog section — embedding is usually the right call. For something large or highly volatile, it may be better for the prompt to just instruct the model to use a tool/resource read case-by-case instead.
Related Resources
It's tempting to bake every good instruction into a system prompt and skip the MCP prompt primitive entirely. That works until you need the same curated interaction to be portable, or explicitly user-triggered, rather than always-on.
Where each belongs
| Use an MCP prompt when... | Use the host's system prompt when... |
|---|---|
| It's a specific, occasional workflow, not always-relevant | It should shape the assistant's behavior for the whole session |
| A user should be able to explicitly pick it from a list/slash command | There's no natural "invocation moment" — it's ambient guidance |
| It should work the same way across any MCP-compatible host | It's specific to one particular application's UX/branding |
| The server author is the domain expert on how to phrase this request | The host author is setting overall assistant tone/persona |
A concrete example
A server for an incident-management tool might expose a triage-incident prompt. Given an incident ID, it returns a carefully worded message instructing the model to pull timeline data, identify the likely root cause category, and draft a first Slack update — codifying the on-call team's actual triage playbook. That's a strong prompt candidate. It's a specific, occasional action a user explicitly triggers, and it should behave identically whether the user is in Claude Desktop, an IDE, or any other MCP host connected to that server.
Compare that to "always respond concisely and never use bullet points unless asked." That's a standing behavioral preference, not a discrete workflow a user invokes. It belongs in the host's system prompt, or a per-app setting, not as a server-defined MCP prompt.
The underlying principle
Prompts exist to make a server author's domain expertise about how to ask well reusable and portable across hosts. System prompts exist to configure how the assistant behaves in general, within one specific application. Conflating the two either buries a genuinely reusable workflow inside one app's private configuration, or clutters a host's general behavior with narrow, occasional-use instructions that most sessions never need.