Skip to main content
MCP (Model Context Protocol) · lesson 5 of 13
MCP (Model Context Protocol) intermediate Lesson 5 of 13

MCP Prompts and Argument Completion

Write MCP prompt templates with required and optional arguments, return multi-message prompts, and suggest argument values with completion/complete.

By SkillByExample · Published

mcp model-context-protocol prompts completion python

Templates the User Picks

Tools are for the model and resources are data. Prompts are message templates the user chooses: a host lists them, often shows them as slash commands or menu items, asks for their arguments and sends the filled-in messages to the model. The server owns the wording, so every client gets the same carefully written prompt.

A Prompt With Arguments

A prompt is a function that returns the text of a user message. Arguments without a default are required.

# prompts.py - a prompt template with one required and one optional argument
import asyncio

from mcp import Client, MCPError
from mcp.server import MCPServer

mcp = MCPServer("review")


@mcp.prompt(title="Review code")
def review(code: str, language: str = "python") -> str:
    """Ask for a focused code review."""
    return f"Review this {language} code for bugs only:\n{code}"


async def main():
    async with Client(mcp) as client:
        prompt = (await client.list_prompts()).prompts[0]
        print(prompt.name, "|", prompt.title, "|", prompt.description)
        print([(a.name, a.required) for a in prompt.arguments])
        result = await client.get_prompt("review", {"code": "total = sum(xs) / len(xs)"})
        for m in result.messages:
            print(m.role, "->", m.content.text)
        try:
            await client.get_prompt("review", {})
        except MCPError as e:
            print("MCPError", e.error.code, e.error.message)


asyncio.run(main())
$ python prompts.py 2>/dev/null
review | Review code | Ask for a focused code review.
[('code', True), ('language', False)]
user -> Review this python code for bugs only:
total = sum(xs) / len(xs)
MCPError -32603 Internal server error

The listing tells a host what to ask for: code is required and language isn’t. get_prompt ran the function and wrapped the returned string in a single message with the user role. language fell back to its default. Prompt arguments always travel as strings, which is why the function’s parameters are str.

The second call left out code, and it failed as a JSON-RPC error with a generic message. -32603 is “internal error”, and the reason (“Missing required arguments: {‘code’}”) went only to the server’s stderr, with a traceback. The SDK treats a missing prompt argument like a crash. Hosts are expected to collect every required argument first, using the required flags from the listing. If you want a clearer error, check the arguments yourself and raise MCPError(-32602, ...), as Practice 3 does.

Multi-Message Prompts

Return a list of messages to start a conversation partway through.

# multi.py - a prompt that returns a short scripted conversation
import asyncio

from mcp import Client
from mcp.server import MCPServer
from mcp.server.mcpserver.prompts.base import AssistantMessage, UserMessage

mcp = MCPServer("support")


@mcp.prompt()
def triage(error: str) -> list[UserMessage | AssistantMessage]:
    """Start a debugging conversation from an error message."""
    return [
        UserMessage(f"My app fails with: {error}"),
        AssistantMessage("Which command did you run, and what changed since it last worked?"),
        UserMessage("python app.py, after upgrading the database driver"),
    ]


async def main():
    async with Client(mcp) as client:
        result = await client.get_prompt("triage", {"error": "KeyError: 'DATABASE_URL'"})
        for m in result.messages:
            print(f"{m.role:>9}: {m.content.text}")


asyncio.run(main())
$ python multi.py
     user: My app fails with: KeyError: 'DATABASE_URL'
assistant: Which command did you run, and what changed since it last worked?
     user: python app.py, after upgrading the database driver

The host receives three messages in order and passes them to the model as conversation history. The assistant message is one the model never wrote. It’s a few-shot example that sets the tone and tells the model which question to ask. Use this to put a conversation in a known state; for a single instruction, a plain string is enough.

Completing Argument Values

A completion handler suggests values while the user types. One handler serves every prompt argument and every URI placeholder.

# complete.py - suggest values for a URI template placeholder
import asyncio

from mcp import Client, types
from mcp.server import MCPServer

mcp = MCPServer("docs")
PAGES = ["faq", "install", "intro", "setup"]


@mcp.resource("docs://pages/{name}")
def page(name: str) -> str:
    return f"page {name}"


@mcp.completion()
async def suggest(ref, argument, context):
    return types.Completion(values=[p for p in PAGES if p.startswith(argument.value)])


async def main():
    async with Client(mcp) as client:
        ref = types.ResourceTemplateReference(type="ref/resource", uri="docs://pages/{name}")
        for typed in ["in", "s", "zz"]:
            result = await client.complete(ref, {"name": "name", "value": typed})
            print(repr(typed), "->", result.completion.values)


asyncio.run(main())
$ python complete.py
'in' -> ['install', 'intro']
's' -> ['setup']
'zz' -> []

The request said which template (ref), which placeholder (name) and what had been typed so far (value). The handler filtered its list by prefix. An empty list is a valid answer, not an error, so a host just shows no suggestions. This is the piece the resources lesson left open: a template can’t list its values, but completion lets a host discover them as the user types. The spec caps a response at 100 values; Completion also has total and has_more fields for when there are more.

Practice

  1. Add the review prompt from prompts.py to complete.py, and make the handler suggest languages from ["go", "python", "rust", "typescript"] when ref is a types.PromptReference. What does completing language with "t" return?
  2. Change the template to docs://{lang}/pages/{name} and complete name using the lang the user already chose. Pass it with client.complete(ref, argument, {"lang": "de"}).
  3. Make review reject an empty or whitespace-only code with a clear -32602 error instead of running.
  4. Give the code argument a description a user would see in a host’s form.
Solution 1
@mcp.completion()
async def suggest(ref, argument, context):
    if isinstance(ref, types.PromptReference) and argument.name == "language":
        pool = ["go", "python", "rust", "typescript"]
    else:
        pool = PAGES
    return types.Completion(values=[v for v in pool if v.startswith(argument.value)])
prompt = types.PromptReference(type="ref/prompt", name="review")
print((await client.complete(prompt, {"name": "language", "value": "t"})).completion.values)

It prints ['typescript']. Check argument.name too: without it, completing code would also suggest languages.

Solution 2
PAGES = {"en": ["setup", "faq"], "de": ["setup", "sicherheit"]}


@mcp.resource("docs://{lang}/pages/{name}")
def page(lang: str, name: str) -> str:
    return f"{lang}/{name}"


@mcp.completion()
async def suggest(ref, argument, context):
    lang = context.arguments.get("lang", "en") if context and context.arguments else "en"
    return types.Completion(values=[p for p in PAGES.get(lang, []) if p.startswith(argument.value)])

With {"lang": "de"}, completing "s" returns ['setup', 'sicherheit']; without context it falls back to English and returns ['setup']. The handler’s context is None when the client sent none, so check before reading .arguments.

Solution 3
@mcp.prompt(title="Review code")
def review(code: str, language: str = "python") -> str:
    """Ask for a focused code review."""
    if not code.strip():
        raise MCPError(-32602, "code must not be empty")
    return f"Review this {language} code for bugs only:\n{code}"

get_prompt("review", {"code": " "}) now raises MCPError -32602 code must not be empty. An MCPError passes through to the client unchanged, while any other exception becomes the generic -32603.

Solution 4
from typing import Annotated

from pydantic import Field


@mcp.prompt(title="Review code")
def review(code: Annotated[str, Field(description="The code to review")], language: str = "python") -> str:

The listing’s argument becomes PromptArgument(name='code', ..., description='The code to review', required=True). It’s the same Field pattern as tool arguments in the tools lesson.

Frequently Asked Questions

What is an MCP prompt?
An MCP prompt is a reusable message template a server offers, with named arguments. A host lists prompts, usually shows them to the user as commands or menu items, asks for the arguments, and calls prompts/get to receive the filled-in messages to send to a model. The user picks a prompt; the model doesn't call it.
Can an MCP prompt return more than one message?
Yes. prompts/get returns a list of messages, each with a role of user or assistant. A server can return a short scripted exchange, for example a user message, an assistant follow-up question and the user's answer, to start a conversation in a known state.
What does completion/complete do in MCP?
It asks the server to suggest values for one argument while a user is typing, like tab completion in a shell. The request names a prompt or a resource template, the argument, and the partial value typed so far. The server returns up to 100 matching values, and a host can show them as a dropdown.
Can MCP completion depend on other arguments?
Yes. A completion request can carry context arguments, the values already chosen for other placeholders. A server completing a page name for docs://{lang}/pages/{name} can read lang from the context and suggest only the pages that exist in that language.