MCP (Model Context Protocol) · lesson 5 of 13
Beginner
4Intermediate
4Advanced
5Also part of MCP (Model Context Protocol)
5MCP 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
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
- Add the
reviewprompt fromprompts.pytocomplete.py, and make the handler suggest languages from["go", "python", "rust", "typescript"]whenrefis atypes.PromptReference. What does completinglanguagewith"t"return? - Change the template to
docs://{lang}/pages/{name}and completenameusing thelangthe user already chose. Pass it withclient.complete(ref, argument, {"lang": "de"}). - Make
reviewreject an empty or whitespace-onlycodewith a clear-32602error instead of running. - Give the
codeargument 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.