Learn Build MCP Servers: Extend Claude with Custom Tools Prompts: Reusable Instruction Templates

Prompts: Reusable Instruction Templates

Advanced 🕐 18 min Lesson 11 of 15
What you'll learn
  • Define reusable MCP prompts with parameterized arguments that clients can offer to users
  • Return properly structured GetPromptResult with messages arrays containing role and content
  • Distinguish when prompts (user-triggered workflows) are more appropriate than tools (model-triggered actions)
  • Embed resource content in prompt messages to automatically pull in relevant context alongside instructions

What MCP Prompts Are

The third MCP primitive — after tools and resources — is the prompt. MCP prompts are pre-defined, parameterized interaction templates that clients surface to users as ready-made workflows. Think of them as canned conversation starters: "Review my code", "Generate a commit message", "Analyze this dataset".

Prompts are not the same as system prompts. They don't configure the model's global behavior. Instead, they are discrete, user-selectable workflows that arrive as a structured messages array — the conversation starter that gets injected into the chat when the user invokes one.

How Prompts Differ from Tools and Resources

The three primitives serve different roles and are triggered in different ways:

  • Tools — the model triggers these automatically when it decides a tool call is appropriate
  • Resources — the model or client reads these to pull in context before responding
  • Prompts — the user selects these explicitly from the client UI, fills in any required arguments, and the client sends the formatted message array to the model

This user-driven nature is the defining characteristic of prompts. The model doesn't decide to invoke a prompt — the user does. This makes prompts ideal for repeatable workflows that developers and power users want to trigger on demand, rather than leaving it to the model to figure out when to apply them.

The Prompt Lifecycle

Here is the sequence when a user invokes a prompt in Claude Desktop:

  1. On startup, the client calls prompts/list — your server returns all available prompts with their names, descriptions, and argument specs
  2. The user picks a prompt from the UI (slash-command menu or attachment panel)
  3. The client presents the argument form to the user and collects values
  4. The client calls prompts/get with the prompt name and argument values
  5. Your server returns a { messages: [...] } object — the conversation starter
  6. The client injects that messages array into the chat and sends it to the model

Prompt Structure

A prompt descriptor (returned in prompts/list) has:

  • name — unique identifier used in prompts/get requests
  • description — shown to the user in the client UI
  • arguments — array of argument specs, each with name, description, and required flag

The prompts/get response contains a messages array. Each message has:

  • role — either "user" or "assistant"
  • content — either a text content object or an embedded resource

TypeScript: Implementing Prompts (Low-Level SDK)

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new McpServer({ name: "dev-tools", version: "1.0.0" });

server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: [
    {
      name: "review_code",
      description: "Review code for bugs, style issues, and improvements",
      arguments: [
        {
          name: "language",
          description: "Programming language",
          required: true,
        },
        {
          name: "focus",
          description: "What to focus on: bugs, style, performance, or all",
          required: false,
        },
      ],
    },
  ],
}));

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "review_code") {
    const focus = args?.focus ?? "all";
    const language = args?.language ?? "code";
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `You are an expert ${language} reviewer. Review the following code focusing on: ${focus}. Be specific about issues and suggest concrete improvements.`,
          },
        },
      ],
    };
  }

  throw new Error(`Unknown prompt: ${name}`);
});

The handler receives args as a plain object of string key-value pairs — the argument values the user filled in. Optional arguments may be undefined, so always provide a default with ??.

Python FastMCP: Prompts with Decorators

FastMCP maps prompt arguments directly to function parameters, matching the resource pattern:

from mcp.server.fastmcp import FastMCP
from mcp.types import GetPromptResult, PromptMessage, TextContent

mcp = FastMCP("dev-tools")

@mcp.prompt()
def review_code(language: str, focus: str = "all") -> list[PromptMessage]:
    """Review code for bugs, style issues, and improvements."""
    return [
        PromptMessage(
            role="user",
            content=TextContent(
                type="text",
                text=f"You are an expert {language} reviewer. Review the following code focusing on: {focus}. Be specific about issues and suggest concrete improvements.",
            ),
        )
    ]

FastMCP infers required/optional from the Python function signature: parameters without defaults are required; parameters with defaults are optional. The docstring becomes the prompt description shown in the client UI.

Multi-Turn Prompts

Return both user and assistant messages to create a conversation starter that includes an example exchange. This is useful for prompts that benefit from showing the model the expected response format before it generates its own:

return {
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: "Generate a conventional commit message for my changes.",
      },
    },
    {
      role: "assistant",
      content: {
        type: "text",
        text: "I'll analyze your diff and produce a commit message following the Conventional Commits specification. Please paste your git diff below.",
      },
    },
  ],
};

The assistant turn primes the model with the right tone and expectations before the actual conversation continues. Use this sparingly — only when the pre-filled assistant turn genuinely improves output quality.

Embedding Resources in Prompts

Prompt messages can include resource content alongside text, automatically pulling in context from a resource URI without requiring the user to attach it manually:

return {
  messages: [
    {
      role: "user",
      content: {
        type: "resource",
        resource: {
          uri: "db://tables/users/schema",
          mimeType: "application/json",
          text: JSON.stringify(usersSchema),
        },
      },
    },
    {
      role: "user",
      content: {
        type: "text",
        text: `Write a SQL query to ${queryGoal}. Use only the columns defined in the schema above.`,
      },
    },
  ],
};

The embedded resource content gets injected inline into the conversation. This pattern is powerful for prompts that always need a specific piece of context — a file, a schema, a config — baked in alongside the instruction.

Practical Use Cases

Prompts work best for repeatable workflows that you find yourself constructing from scratch over and over:

  • Code review — parameterized by language and focus area
  • Git commit messages — takes a diff, returns a formatted conventional commit
  • Data analysis starters — loads a dataset resource and asks for specific analysis
  • Debugging walkthroughs — parameterized by language and error message
  • Documentation drafts — takes a function or class and produces docstring or README section
  • Test generation — parameterized by framework (Jest, pytest, etc.)

If you're writing the same opening instruction in Claude again and again, that's a prompt waiting to be built.

Client Support

Claude Desktop displays prompts in the slash-command menu (type / in the chat input) or in the attachment panel, depending on the client version. When a user selects a prompt, Claude Desktop presents any required arguments as a form before sending the prompts/get request. You can verify your prompts are visible and callable in MCP Inspector before testing in Claude Desktop — the Inspector has a dedicated Prompts tab where you can invoke them with custom argument values.

What to Try Next

Add a prompt to one of the servers you built earlier in this track:

  1. Define a git_commit prompt with a required diff argument and an optional style argument defaulting to "conventional"
  2. Return a single user message that includes both the diff and an instruction to generate the commit message in the requested style
  3. Open MCP Inspector, go to the Prompts tab, fill in a sample diff, and verify the returned message array looks correct before connecting to Claude Desktop
Key takeaways
  • Prompts are user-triggered, not model-triggered — the user picks one from the client UI, fills in arguments, and the client sends the formatted message to the model
  • A prompt returns a messages array with role and content — this is the conversation starter that gets injected into the chat
  • Arguments have required and description fields — required arguments must be provided before the prompt can be invoked
  • Embed resource content in prompt messages to automatically pull in relevant context (e.g., the current file or database schema) alongside the instructions
  • Prompts work best for repeatable workflows: code review, commit messages, data analysis starters, debugging checklists — anything you find yourself writing from scratch repeatedly