Module 3: Three Primitives — Resources, Tools, Prompts

Prompts: Reusable Templates with Parameters

Prompts: Reusable Templates with Parameters

Capsule description

The third MCP primitive is the least intuitive for developers: Prompts. They're not functions that run code or data that's read. They're parameterized templates that standardize how the user interacts with the model through the MCP server.

Think of it this way: if a Resource is what the model can see and a Tool is what the model can do, a Prompt is how the user asks for things. It's the difference between typing "review my code" (vague) and selecting a "Code Review" template that already includes the review criteria, the output format, and the aspects to evaluate.

Prompts are like predefined recipes: the user picks one, fills in the parameters, and gets a complete, optimized prompt ready for the model. They reduce friction and ensure consistency.


What is a Prompt in MCP?

Formal definition

A Prompt in MCP is a reusable template with optional parameters that the server exposes to the client. When the user selects a prompt:

  1. The server lists the available prompts (prompts/list)
  2. The user selects a prompt and fills in the parameters
  3. The server generates the prompt's messages with the parameters applied (prompts/get)
  4. The messages are inserted into the conversation with the model

Practical definition

A prompt answers: "What predefined interactions does this server offer?"

Examples of prompts:
├── code-review          → Code review with standard criteria
├── refactoring-plan     → Step-by-step refactoring plan
├── sql-query-generator  → SQL query generator from natural language
├── bug-report           → Standardized bug report template
├── api-documentation    → API documentation generator
└── test-generator       → Test generator for a function

Key characteristics

CharacteristicDetail
TemplateText with placeholders filled in with parameters
ParametersArguments the user provides (required or optional)
Without side effectsGenerating a prompt doesn't modify state
Controlled by the userThe user chooses and parameterizes — not the model
Multi-messageCan generate multiple messages (system, user, assistant)
Embedded contextCan include resources as context within the prompt

Why do you need Prompts?

The problem they solve

Without prompts, each user writes their instructions differently:

User A: "review this code"
User B: "do a code review of the file main.py"
User C: "analyze the code looking for bugs, performance issues, and readability improvements"

User C gets better results because their prompt is more specific. But why should each user reinvent the perfect prompt?

The solution: standardized prompts

Prompt: "code-review"
Parameters: { file: "main.py", focus: "security" }

→ Automatically generates:
  "Perform a code review of the file main.py with a focus on security.
   Evaluate: SQL injection, XSS, secret handling, input validation.
   Format: list of issues with severity (critical/high/medium/low)."

All users get the same quality of instruction. The knowledge of how to ask for a good code review is encapsulated in the prompt.


Anatomy of a Prompt

Definition structure

interface PromptDefinition {
  name: string;                // "code-review"
  description?: string;        // "Performs a professional code review"
  arguments?: Array<{
    name: string;              // "filePath"
    description?: string;      // "Path of the file to review"
    required?: boolean;        // true
  }>;
}

Response structure

When the client requests a prompt, the server returns a list of messages:

interface GetPromptResult {
  description?: string;
  messages: Array<{
    role: "user" | "assistant";
    content: {
      type: "text" | "resource";
      text?: string;
      resource?: {             // Embedded resource
        uri: string;
        mimeType?: string;
        text?: string;
      };
    };
  }>;
}

A prompt can return multiple messages

This is a powerful feature. A prompt doesn't just generate a user message — it can generate a sequence of messages that "prepares" the conversation:

messages: [
  {
    role: "user",
    content: {
      type: "text",
      text: "You are an expert in web application security."
    }
  },
  {
    role: "user",
    content: {
      type: "resource",
      resource: {
        uri: `file:///${filePath}`,
        text: fileContent,
        mimeType: "text/plain"
      }
    }
  },
  {
    role: "user",
    content: {
      type: "text",
      text: "Analyze this code looking for security vulnerabilities..."
    }
  }
]

Implementation in TypeScript

Basic prompt

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

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

server.prompt(
  "code-review",
  "Performs a professional code review of a file",
  {
    filePath: z.string().describe("Path of the file to review"),
    focus: z.enum(["general", "security", "performance", "readability"])
      .default("general")
      .describe("Focus area of the review"),
  },
  async ({ filePath, focus }) => {
    const criteria = {
      general: "bugs, code improvements, patterns, naming, and structure",
      security: "SQL injection, XSS, secret handling, input validation, and authentication",
      performance: "algorithmic complexity, memory usage, N+1 queries, and caching",
      readability: "naming, comments, structure, single responsibility principle, and clarity",
    };

    return {
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text" as const,
            text: `Perform a professional code review of the file ${filePath}.

**Main focus:** ${focus}

**Evaluation criteria:**
${criteria[focus]}

**Response format:**
For each issue found, report:
1. **Line(s):** line number or range
2. **Severity:** 🔴 Critical | 🟠 High | 🟡 Medium | 🔵 Low
3. **Description:** what's wrong and why
4. **Suggestion:** how to fix it with example code

At the end, include:
- **Summary:** X issues found (N critical, N high, etc.)
- **Overall score:** 1-10
- **Top 3 priority improvements**`,
          },
        },
      ],
    };
  }
);

Prompt with an embedded resource

server.prompt(
  "refactoring-plan",
  "Generates a refactoring plan for a file",
  {
    filePath: z.string().describe("Path of the file to refactor"),
    goal: z.string().describe("What you want to improve (e.g., 'separate responsibilities')"),
  },
  async ({ filePath, goal }) => {
    const fs = await import("fs/promises");
    let content: string;

    try {
      content = await fs.readFile(filePath, "utf-8");
    } catch {
      content = `[Error: could not read ${filePath}]`;
    }

    return {
      messages: [
        {
          role: "user" as const,
          content: {
            type: "resource" as const,
            resource: {
              uri: `file:///${filePath}`,
              text: content,
              mimeType: "text/plain",
            },
          },
        },
        {
          role: "user" as const,
          content: {
            type: "text" as const,
            text: `Generate a refactoring plan for this file.

**Goal:** ${goal}

**The plan must include:**
1. **Current analysis:** what the code does and what problems it has
2. **Refactoring strategy:** general approach
3. **Specific steps:** ordered list of changes
4. **Suggested code:** snippets of how each part would look
5. **Risks:** what could break and how to mitigate it
6. **Tests:** what tests to add before refactoring`,
          },
        },
      ],
    };
  }
);

Prompt for code generation

server.prompt(
  "generate-api-endpoint",
  "Generates code for a REST API endpoint",
  {
    resource: z.string().describe("Resource name (e.g., 'users', 'products')"),
    operations: z.string().describe("Operations to generate: 'CRUD' or a list like 'create,read'"),
    framework: z.enum(["express", "fastapi", "hono"]).default("express").describe("Framework to use"),
  },
  async ({ resource, operations, framework }) => {
    const ops = operations.toLowerCase() === "crud"
      ? ["create", "read", "update", "delete"]
      : operations.split(",").map(o => o.trim());

    return {
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text" as const,
            text: `Generate code for an API endpoint for the resource "${resource}" with the ${framework} framework.

**Required operations:** ${ops.join(", ")}

**Requirements:**
- Complete input validation
- Error handling with appropriate HTTP codes
- Full typing (TypeScript if Express/Hono, type hints if FastAPI)
- Explanatory comments in English
- Production-ready structure

**For each operation generate:**
1. Route handler / endpoint
2. Validation of request body/params
3. Response with the appropriate status code
4. Error handling (404, 400, 500)`,
          },
        },
      ],
    };
  }
);

Implementation in Python

Basic prompt

from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent

server = FastMCP("dev-tools")

@server.prompt()
async def code_review(file_path: str, focus: str = "general") -> str:
    """Performs a professional code review of a file.

    Args:
        file_path: Path of the file to review
        focus: Focus area (general, security, performance, readability)
    """
    criteria = {
        "general": "bugs, code improvements, patterns, naming, and structure",
        "security": "SQL injection, XSS, secret handling, input validation",
        "performance": "algorithmic complexity, memory usage, N+1 queries, caching",
        "readability": "naming, comments, structure, single responsibility, clarity",
    }

    return f"""Perform a professional code review of the file {file_path}.

**Main focus:** {focus}

**Evaluation criteria:**
{criteria.get(focus, criteria["general"])}

**Response format:**
For each issue:
1. Affected line(s)
2. Severity: Critical | High | Medium | Low
3. Description of the problem
4. Suggestion with example code

Final summary: total issues, score 1-10, top 3 priorities."""

Prompt with dynamic context

@server.prompt()
async def explain_error(error_message: str, language: str = "python") -> str:
    """Explains a programming error and suggests solutions.

    Args:
        error_message: The complete error message
        language: Programming language (python, typescript, rust)
    """
    return f"""Analyze this {language} error and help me solve it:

{error_message}


**Respond with:**
1. **What it means:** Explanation in English, without unnecessary jargon
2. **Why it happens:** The most common causes of this error
3. **How to solve it:** Step-by-step solution with code
4. **How to prevent it:** What to do so it doesn't happen again
5. **Example:** Minimal code that reproduces and solves the error"""

Prompt for tests

@server.prompt()
async def generate_tests(file_path: str, framework: str = "pytest") -> str:
    """Generates tests for a file's functions.

    Args:
        file_path: Path of the file to test
        framework: Testing framework (pytest, unittest, jest, vitest)
    """
    return f"""Generate complete tests for the functions in {file_path} using {framework}.

**For each function generate:**
1. Happy path test (normal case)
2. Edge case tests (empty, null, extreme inputs)
3. Error case tests (invalid inputs, expected exceptions)
4. Return type test

**Structure:**
- Descriptive names: test_[function]_[scenario]_[expected_result]
- Arrange-Act-Assert pattern
- Fixtures/mocks when necessary
- Comments explaining the why of each test"""

Prompts vs direct Instructions

Why not just write the prompt?

You could argue: "I can write these instructions myself. Why encapsulate them in a Prompt?"

AspectWriting every timeMCP Prompt
ConsistencyVaries with the day and the rushAlways the same quality
EfficiencyRewriting is repeated workSelect + parameters
KnowledgeDepends on what you rememberThe expertise is in the template
SharingHard to share with the teamAvailable to everyone on the server
EvolutionEveryone improves on their ownCentralized improvements benefit everyone
ContextYou forget to include files/dataCan embed resources automatically

When to use Prompts vs Tools

Does the user need the model to EXECUTE something?
  → Yes → Tool
  → No

Does the user need data as CONTEXT?
  → Yes → Resource
  → No

Does the user want a STANDARDIZED way to ask for something?
  → Yes → Prompt

Prompts don't replace tools or resources — they complement them. A prompt can include resources as context and generate instructions that lead the model to use tools.


Advanced patterns

Pattern 1: Prompt with multiple roles

server.prompt(
  "pair-programming",
  "Starts an assisted pair programming session",
  {
    taskDescription: z.string().describe("Description of the task to implement"),
    expertise: z.enum(["junior", "mid", "senior"]).default("mid").describe("The developer's level"),
  },
  async ({ taskDescription, expertise }) => {
    const level = {
      junior: "Explain each decision in detail, show alternatives, include basic best practices",
      mid: "Focus on design decisions, patterns, and trade-offs. Assume knowledge of the syntax",
      senior: "Be concise, focus on architecture, edge cases, and performance. Suggest without explaining the obvious",
    };

    return {
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text" as const,
            text: `Let's do pair programming. I'm the driver, you're the navigator.

**Task:** ${taskDescription}

**My level:** ${expertise}
**Your style:** ${level[expertise]}

**Session rules:**
1. Don't write the complete code all at once — guide me step by step
2. After each step, wait for my implementation before continuing
3. If I make a mistake, point it out and explain why
4. Suggest tests for each piece of functionality
5. At the end, make a summary of what we built

Shall we start? Give me the first step.`,
          },
        },
      ],
    };
  }
);

Troubleshooting

"The prompt doesn't appear in the list of prompts"

Cause: The prompt wasn't registered correctly or the host doesn't support prompts.

Solution:

# Verify with MCP Inspector
npx @modelcontextprotocol/inspector

# Look for the "Prompts" section — it should list your registered prompts
# If they don't appear, verify that server.prompt() is called before connect()

"The parameters aren't passed correctly"

Cause: The parameter name in the definition doesn't match the one the client uses.

Solution:

// Verify that the parameter names are consistent
server.prompt(
  "my-prompt",
  "Description",
  {
    filePath: z.string(),  // ← this name
  },
  async ({ filePath }) => {  // ← must match here
    // ...
  }
);

"The prompt generates empty messages"

Cause: The function returns an empty messages array or one with undefined content.

Solution:

// Make sure you always return at least one message with content
return {
  messages: [
    {
      role: "user" as const,
      content: {
        type: "text" as const,
        text: "This text must never be empty",
      },
    },
  ],
};

"The embedded resource isn't included"

Cause: The file's path is incorrect or the file doesn't have read permissions.

Solution:

// Add error handling when reading files for the prompt
try {
  const content = await fs.readFile(filePath, "utf-8");
  // ... use content
} catch (error) {
  // Fallback: inform the model that it couldn't be read
  messages.push({
    role: "user",
    content: {
      type: "text",
      text: `[Could not read ${filePath}: ${error}. Proceed without the file.]`,
    },
  });
}

Exercises

Exercise 1: Identify Prompt candidates (Easy)

From this list of common interactions, identify which would be good candidates for an MCP Prompt:

  1. "Explain this Python error"
  2. "Create a configuration file"
  3. "Do a code review focused on performance"
  4. "What time is it?"
  5. "Generate documentation for this function"
  6. "Delete the temporary files"
See solution
  1. ✅ Prompt — It's a repeatable pattern: "given an error, explain and suggest a solution" with a consistent format
  2. ❌ Tool — Creates a file (side effect), it's not an interaction template
  3. ✅ Prompt — Code review with standardized criteria is an ideal case for a prompt
  4. ❌ Neither prompt nor resource/tool — Doesn't require MCP
  5. ✅ Prompt — Generating documentation with a consistent format is a repeatable pattern
  6. ❌ Tool — Deletes files (side effect)

Rule: A good Prompt candidate is an interaction that repeats frequently, benefits from a consistent structure/format, and where the value is in "how you ask," not in "what you execute."

Exercise 2: Design a Prompt's parameters (Medium)

Design the parameters (name, type, description, required/optional) for these prompts:

  1. A "Commit Message Generator" prompt
  2. A "SQL Query Generator" prompt
See solution
// 1. Commit Message Generator
server.prompt(
  "commit-message",
  "Generates a commit message following conventional commits",
  {
    diff: z.string().describe("Output of 'git diff --staged' with the changes"),
    type: z.enum(["feat", "fix", "refactor", "docs", "test", "chore"])
      .optional()
      .describe("Commit type. If not specified, it's inferred from the diff"),
    scope: z.string().optional().describe("Scope of the change (e.g., 'auth', 'api', 'ui')"),
    language: z.enum(["en", "es"]).default("en").describe("Language of the message"),
  },
  async ({ diff, type, scope, language }) => { /* ... */ }
);

// 2. SQL Query Generator
server.prompt(
  "sql-query",
  "Generates a SQL query from a natural language description",
  {
    description: z.string().describe("What data you need, in natural language"),
    dialect: z.enum(["postgresql", "mysql", "sqlite"]).default("postgresql").describe("SQL dialect"),
    tables: z.string().describe("Names of available tables, comma-separated"),
    includeExplanation: z.boolean().default(true).describe("Include an explanation of the query"),
  },
  async ({ description, dialect, tables, includeExplanation }) => { /* ... */ }
);

Exercise 3: Implement a Prompt in TypeScript (Medium)

Implement the "commit-message" prompt from the previous exercise with the complete logic:

See solution
server.prompt(
  "commit-message",
  "Generates a commit message following conventional commits",
  {
    diff: z.string().describe("Output of 'git diff --staged'"),
    type: z.enum(["feat", "fix", "refactor", "docs", "test", "chore"])
      .optional()
      .describe("Commit type (inferred if not specified)"),
    scope: z.string().optional().describe("Scope of the change"),
    language: z.enum(["en", "es"]).default("en").describe("Language of the message"),
  },
  async ({ diff, type, scope, language }) => {
    const langInstruction = language === "es"
      ? "Escribe el mensaje en español"
      : "Write the message in English";

    const scopeStr = scope ? `(${scope})` : "";
    const typeStr = type ? `Type: ${type}` : "Infer the type based on the changes";

    return {
      messages: [
        {
          role: "user" as const,
          content: {
            type: "text" as const,
            text: `Generate a commit message for these changes following Conventional Commits.

**Diff:**
\`\`\`diff
${diff}
\`\`\`

**Instructions:**
- ${typeStr}
- Scope: ${scopeStr || "infer from the context"}
- ${langInstruction}
- Format: \`type(scope): brief description\`
- The description must be imperative ("add", not "added")
- Maximum 72 characters in the first line
- If necessary, add a body with more context
- If there are breaking changes, include them

**Return ONLY the commit message, without additional explanations.**`,
          },
        },
      ],
    };
  }
);

Exercise 4: Prompt with an embedded Resource in Python (Medium)

Implement a prompt in Python that reads a file and generates documentation for it:

See solution
from mcp.server.fastmcp import FastMCP
from mcp.types import UserMessage, TextContent

server = FastMCP("doc-generator")

@server.prompt()
async def generate_docs(file_path: str, doc_style: str = "jsdoc") -> list:
    """Generates documentation for a file's functions.

    Args:
        file_path: Path of the file to document
        doc_style: Documentation style (jsdoc, numpy, google, sphinx)
    """
    try:
        with open(file_path, "r") as f:
            content = f.read()
    except FileNotFoundError:
        content = f"[Could not read the file: {file_path}]"

    styles = {
        "jsdoc": "JSDoc (/** @param {type} name - description */)",
        "numpy": "NumPy style (Parameters\\n----------)",
        "google": "Google style (Args:\\n    param: description)",
        "sphinx": "Sphinx style (:param name: description)",
    }

    return [
        UserMessage(content=TextContent(
            type="text",
            text=f"File to document ({file_path}):\n\n```\n{content}\n```"
        )),
        UserMessage(content=TextContent(
            type="text",
            text=f"""Generate complete documentation for this file.

**Style:** {styles.get(doc_style, doc_style)}

**For each function/class document:**
1. Description of what it does (one line)
2. Detailed description (if applicable)
3. Parameters with type and description
4. Return value with type and description
5. Exceptions it can throw
6. Usage example

**Rules:**
- Documentation in English
- Include precise types, not 'any' or 'object'
- The examples must be runnable"""
        )),
    ]

Summary

In this capsule you learned:

  • Prompts are reusable templates with parameters that standardize interactions
  • They encapsulate expertise — the knowledge of "how to ask well" lives in the template
  • Controlled by the user — unlike tools, the user chooses when to use a prompt
  • They can generate multiple messages including embedded resources as context
  • In TypeScript they're registered with server.prompt() and Zod for parameters
  • In Python they're registered with the @server.prompt() decorator and type hints
  • They're ideal for: code reviews, doc generation, commit templates, refactoring plans
  • They don't replace Tools or Resources — they complement them by standardizing how things are asked for

Next capsule: Combining Primitives — how Resources, Tools, and Prompts work together in a real MCP server.


Additional resources

  1. MCP Specification — Prompts - Official Prompts specification
  2. MCP TypeScript SDK — Prompts - Implementation in TypeScript
  3. MCP Python SDK — Prompts - Implementation in Python
  4. Prompt Engineering Guide - Prompt engineering techniques for better templates
  5. Conventional Commits - Standard used in the commit messages example
  6. MCP Inspector - Tool to test prompts interactively