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:

PrimitiveWhat it returnsClient invocationBest for
PromptArray of LLM messagesprompts/getReusable interaction flows, guided workflows
ToolText or structured result of an actiontools/callComputations, writes, external API calls
ResourceContent with MIME type (data artifact)resources/readFiles, 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/listprompts/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