Guide · MCP Protocol
MCP server prompts API
The MCP Prompts protocol lets your server expose reusable, parameterized prompt templates that clients can invoke by name. Unlike tools (which run code) or resources (which expose data), prompts return structured message arrays — user and assistant turns — that the client injects into the LLM conversation. This is how MCP servers deliver curated, server-controlled interaction patterns that work across any MCP-compatible client.
TL;DR
Register prompts with server.prompt(). Each prompt has a name, description, an optional argument schema, and a handler that returns a messages array. Messages are role-tagged objects (role: 'user' | 'assistant') with a content field containing text or embedded resource references. Clients call prompts/list to discover available prompts and prompts/get with argument values to retrieve the rendered message array. When your prompt catalog changes at runtime, call server.sendPromptListChanged().
Prompts vs tools vs resources
The three MCP primitives each serve a distinct purpose:
| Primitive | What it returns | Client invocation | Best for |
|---|---|---|---|
| Prompt | Array of LLM messages | prompts/get | Reusable interaction flows, guided workflows |
| Tool | Text or structured result of an action | tools/call | Computations, writes, external API calls |
| Resource | Content with MIME type (data artifact) | resources/read | Files, DB records, live data |
Use prompts when you want the LLM to follow a specific multi-step interaction pattern — a code review flow, a debugging interview, a data extraction template — with server-side control over the message structure.
Registering a simple prompt
The minimal prompt: a name and a handler that returns a messages array.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const server = new McpServer({ name: 'my-server', version: '1.0.0' });
server.prompt(
'summarize',
'Summarize a piece of text concisely',
async () => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'Please summarize the following text in 3 bullet points.',
},
},
],
})
);
The role field is either 'user' or 'assistant'. Most prompts start with a user message. Assistant messages in the array allow you to provide example completions or to pre-populate part of the conversation.
Prompts with arguments
Use an argument schema to declare what parameters the client must supply when calling prompts/get. Arguments are strings — the client is responsible for coercing user input to strings before passing them.
import { z } from 'zod';
server.prompt(
'code-review',
'Structured code review prompt for a specific language and focus area',
{
language: z.string().describe('Programming language of the code'),
focus: z.enum(['security', 'performance', 'readability', 'correctness'])
.describe('Primary review focus'),
},
async ({ language, focus }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: [
`You are an expert ${language} code reviewer.`,
`Focus your review on ${focus}.`,
`Structure your feedback as:`,
`1. Critical issues (must fix)`,
`2. Suggested improvements`,
`3. Positive observations`,
``,
`Review the code I am about to share.`,
].join('\n'),
},
},
],
})
);
The Zod schema generates the argument definitions that appear in the prompts/list response. Mark arguments as optional with z.string().optional(). Required arguments that are not provided cause the handler to throw, which the SDK converts to a protocol error.
Dynamic prompts from live data
Prompt handlers are async functions — you can query a database, call an API, or read files to build the message content dynamically.
server.prompt(
'incident-analysis',
'Analyzes the current open incidents in the monitoring system',
async () => {
// Fetch live data inside the handler
const incidents = await db.incidents.findOpen();
const incidentList = incidents.map(i =>
`- [${i.severity.toUpperCase()}] ${i.title} (open since ${i.openedAt})`
).join('\n');
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: [
'Here are the currently open incidents:',
'',
incidentList,
'',
'Please analyze these incidents and identify:',
'1. Which are most urgent and why',
'2. Any patterns or common root causes',
'3. Recommended prioritization order',
].join('\n'),
},
},
],
};
}
);
Embedding resources in prompts
Messages can reference resources from your server by embedding a resource reference in the content. This lets you build prompts that pull live data into the LLM context automatically.
server.prompt(
'analyze-config',
'Analyze the current application configuration for issues',
async () => ({
messages: [
{
role: 'user',
content: {
type: 'resource',
resource: {
uri: 'config://app/settings',
mimeType: 'application/json',
text: JSON.stringify(await readCurrentConfig(), null, 2),
},
},
},
{
role: 'user',
content: {
type: 'text',
text: 'Review the configuration above. Identify any security issues, performance concerns, or misconfigurations.',
},
},
],
})
);
A single message can contain multiple content items by wrapping them in an array. Combine text and resource references to build rich context-injecting prompts.
Multi-turn prompts with assistant prefill
Include assistant-role messages to establish a conversational context or to provide example responses the LLM should follow stylistically.
server.prompt(
'debugging-interview',
'Systematic debugging interview to diagnose a reported bug',
{
bugDescription: z.string().describe('Brief description of the reported bug'),
},
async ({ bugDescription }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `I have a bug to report: ${bugDescription}`,
},
},
{
role: 'assistant',
content: {
type: 'text',
text: "I'll help you diagnose this systematically. Let me ask a few questions to narrow down the root cause.",
},
},
{
role: 'user',
content: {
type: 'text',
text: 'What information do you need first?',
},
},
],
})
);
Notifying clients when prompts change
If your server's prompt catalog changes at runtime — new prompts added, old prompts removed, argument schemas updated — call sendPromptListChanged() to trigger a fresh prompts/list from connected clients.
// After updating the prompt registry at runtime
await registerNewPromptFromConfig(newConfig);
server.sendPromptListChanged();
Testing prompts
Prompts are pure async functions — test them directly without a transport by calling the handler in isolation. Use InMemoryTransport for end-to-end tests that verify the full prompts/list → prompts/get flow.
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
const client = new Client({ name: 'test', version: '1.0' }, {});
await client.connect(clientTransport);
const { prompts } = await client.listPrompts();
expect(prompts.find(p => p.name === 'code-review')).toBeDefined();
const result = await client.getPrompt({
name: 'code-review',
arguments: { language: 'TypeScript', focus: 'security' },
});
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('user');
Further reading
- MCP server resources API — expose structured data to LLM clients
- MCP tool design — naming, argument schemas, and return shapes
- MCP server sampling — LLM inference requests through the client
- MCP tool annotations — hints for safe and destructive tool calls
- MCP server testing — InMemoryTransport and unit test patterns
- MCP server Zod validation — schema enforcement for tool inputs
- MCP server error handling — protocol errors and handler failures
- MCP server Streamable HTTP transport — remote deployment
- AliveMCP — uptime monitoring for HTTP-deployed MCP servers