Menu

AI

Lesson 17: Add Prompts

Add reusable prompt templates to an MCP server in Python and TypeScript, with titles, described arguments and multi-message starts users pick by name.

By: Irina ShvayaOctober 28, 2026

Lesson 17: Add Prompts

This lesson is part of Learn MCP, our free course on the Model Context Protocol, within The MCP Guide.

In this lesson you will add prompts to the bookshop server. You will write a one-message prompt, a multi-message prompt that seeds the model's reply, and the same prompt in TypeScript. You will also see what happens when a user leaves out a required argument.

A prompt is a message template the user picks by name, often as a slash command or a menu item in the host. The model does not choose prompts. The Python SDK's Prompts guide describes them as "a message template the user picks."

The code adapts that guide and the TypeScript Prompts guide. It ran on 28 October 2026 with mcp 2.3.0 and @modelcontextprotocol/server 2.4.0.

Key Takeaways

  • Prompts are user-controlled: the host lists them, the user picks one and fills in its arguments.
  • In Python, @mcp.prompt() on a function that returns a string makes a one-message prompt.
  • Return a list of UserMessage and AssistantMessage objects to seed a conversation, including the start of the model's reply.
  • Prompt arguments are named strings with a description and a required flag, not a full JSON Schema.
  • A missing required argument fails the whole request with a protocol error; there is no tool-style error result.

Step 1: Pick prompts worth shipping

A good prompt captures a task your users repeat and that benefits from your server's context. The prompts spec says prompts are designed to be user-controlled, exposed so users can explicitly select them.

For the bookshop, two candidates:

  • "Recommend similar books": takes a title and an optional count.
  • "Write a back-cover blurb": takes a title and an optional audience, and steers the reply's opening.

Avoid prompts that only rephrase what a user could type. A prompt earns its place when it adds structure, constraints or context the user would forget.

Step 2: Write a one-message prompt in Python

from typing import Annotated

from pydantic import Field

from mcp.server import MCPServer

mcp = MCPServer("Bookshop")


@mcp.prompt(title="Recommend similar books")
def recommend(
    title: Annotated[str, Field(description="A book the reader enjoyed.")],
    count: Annotated[str, Field(description="How many picks, as a number.")] = "3",
) -> str:
    """Ask for books similar to one the reader enjoyed."""
    return f"Recommend {count} books for someone who enjoyed {title}. Explain each pick in one line."

How the SDK reads it, per the Prompts guide:

  • The function name is the prompt name; the docstring is the description.
  • title= is the human-readable label a host puts on the button or menu entry.
  • Each parameter becomes an argument. One with no default is required.
  • Field(description=...) becomes the argument's description, which hosts show next to the input box.
  • The returned string becomes one message with the user role.

Notice that count is a string. The guide explains that prompt arguments are "a flat list of named string values," because a person fills them in, not a model.

Step 3: Seed a conversation with several messages

Return a list of messages to set up more than one turn:

from mcp.server.mcpserver.prompts.base import AssistantMessage, Message, UserMessage


@mcp.prompt(title="Write a back-cover blurb")
def blurb(title: str, audience: str = "general readers") -> list[Message]:
    """Draft a short back-cover blurb for a book."""
    return [
        UserMessage(f"Write a 60-word back-cover blurb for {title}, aimed at {audience}."),
        UserMessage("Avoid spoilers past the first chapter."),
        AssistantMessage("Here is a spoiler-free blurb:"),
    ]

The last message has the assistant role. The Prompts guide calls this pre-filling an assistant turn: it steers the model's next reply without making the user type the steering.

Step 4: Test the prompts

import anyio
from mcp import Client, MCPError

from server import mcp


async def main() -> None:
    async with Client(mcp) as client:
        for p in (await client.list_prompts()).prompts:
            print(p.name, "|", p.title, "|", [(a.name, a.required) for a in p.arguments])
        got = await client.get_prompt("blurb", {"title": "Dune"})
        for m in got.messages:
            print(m.role, ":", m.content.text)
        try:
            await client.get_prompt("recommend", {})
        except MCPError as e:
            print("MCPError", e.error.code, e.error.message)


anyio.run(main)

The output on 28 October 2026:

recommend | Recommend similar books | [('title', True), ('count', False)]
blurb | Write a back-cover blurb | [('title', True), ('audience', False)]
user : Write a 60-word back-cover blurb for Dune, aimed at general readers.
user : Avoid spoilers past the first chapter.
assistant : Here is a spoiler-free blurb:
MCPError -32603 Internal server error

That last line needs a comment. The SDK docs say a missing required argument fails with -32602 (Invalid params). In the released mcp 2.3.0, the check raised a plain ValueError instead, which the client saw as -32603 "Internal server error." The SDK's main branch already raises the documented -32602, so expect that in a later release.

Either way, the lesson is the same. A prompt with a missing required argument fails as a protocol error. Your host handles it, and no model ever sees it.

Step 5: Write the same prompt in TypeScript

In TypeScript, registerPrompt takes a name, a config with an argsSchema, and a callback that returns { messages }:

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

function createServer(): McpServer {
    const server = new McpServer({ name: 'bookshop', version: '1.0.0' });

    server.registerPrompt(
        'recommend',
        {
            title: 'Recommend similar books',
            description: 'Ask for books similar to one in the catalog',
            argsSchema: z.object({
                title: z.string().describe('A book the reader enjoyed'),
                count: z.string().optional().describe('How many picks, as a number')
            })
        },
        ({ title, count }) => ({
            messages: [
                {
                    role: 'user' as const,
                    content: {
                        type: 'text' as const,
                        text: `Recommend ${count ?? '3'} books for someone who enjoyed ${title}. Explain each pick in one line.`
                    }
                }
            ]
        })
    );

    return server;
}

Serve it with serveStdio(createServer) as in lesson 15. The TypeScript guide notes that .describe() text survives into prompts/list as the argument description.

Check it with the Inspector:

npx @modelcontextprotocol/inspector --cli npx tsx src/resources.ts \
  --protocol-era modern --method prompts/get \
  --prompt-name recommend --prompt-args title=Dune --format json

Leaving out title failed with "Invalid arguments for prompt recommend: title: Invalid input: expected string, received undefined." The TypeScript guide says this is a protocol error with code -32602.

Step 6: Keep prompts honest

Prompts run inside a user's conversation, so treat them like UI copy:

  • Say what the prompt does in its title. "Recommend similar books" beats "rec_v2."
  • Keep arguments few. Every argument is a field a person must fill in.
  • Do not hide instructions. A prompt that quietly tells the model to ignore the user, or to call unrequested tools, breaks trust. The prompts spec says implementations must carefully validate prompt inputs and outputs to prevent injection attacks.
  • Version deliberately. Renaming a prompt breaks users who learned the old slash command.

Try it

  1. Add a prompt called compare that takes two titles and asks for a short comparison table.
  2. Give both arguments descriptions and make both required.
  3. Add an AssistantMessage that starts the table header, such as "| Aspect | Book A | Book B |".
  4. List prompts and render compare with the in-memory client.
  5. Render it with only one title and note the error code your SDK version returns.

Recap

  • Prompts are user-picked templates; tools are model-picked; resources are app-loaded.
  • Python: @mcp.prompt() returning a str or a list of messages. TypeScript: registerPrompt with an argsSchema.
  • Use titles and argument descriptions; hosts show both to people.
  • Missing required arguments fail the request as a protocol error.

Next step

Lesson 18 shows how a tool can stop and ask the user a question. For the protocol view of prompts, read MCP prompts explained and argument completion, which lets hosts autocomplete prompt arguments. The full course outline is on the Learn MCP page.

Course navigation

Previous lesson: Lesson 16: Add Resources.

Next lesson: Lesson 18: Ask the User With Elicitation.

Back to the Learn MCP course outline.

Frequently asked questions

Can a prompt call a tool?

No. A prompt only produces messages. The model may decide to call tools after reading them, but the prompt itself runs no code beyond building the messages.

Can a prompt include a file or an image?

Yes. The Python Prompts guide shows wrapping an embedded resource or an Image helper in a UserMessage. The TypeScript guide says message content accepts text, image, audio, resource links and embedded resources.

Why are prompt arguments strings only?

Because people fill them in through a form, not a model building JSON. The SDK docs describe them as named string values. Parse numbers inside your function if you need them.

Do all hosts show prompts?

No. Support varies by host. Check your target host's docs before you invest in many prompts, and make sure your server is still useful through tools alone.

Can I add prompts while the server is running?

Yes. The Python guide shows adding and removing prompts at runtime, then notifying clients that the prompt list changed so they fetch it again.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.