Completions
A client building a UI on top of your server wants to autocomplete argument values as the user types: language names, repository names, file paths.
Completions are how your server supplies those suggestions.
Something worth completing
Completions apply to exactly two things: the arguments of a prompt and the parameters of a resource template. So start with a server that has one of each:
from mcp.server import MCPServer
mcp = MCPServer("GitHub Explorer")
@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
"""A GitHub repository."""
return f"Repository: {owner}/{repo}"
@mcp.prompt()
def review_code(language: str, code: str) -> str:
"""Review a snippet of code."""
return f"Review this {language} code:\n{code}"
Nothing here is about completions yet.
review_codetakes alanguage. A user shouldn't have to guess which spellings you accept.github_repotakes anownerand arepo. Free-text boxes for both make a bad form.
The completion handler
Add one function decorated with @mcp.completion():
from mcp_types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
from mcp.server import MCPServer
mcp = MCPServer("GitHub Explorer")
LANGUAGES = ["go", "javascript", "python", "rust", "typescript"]
@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
"""A GitHub repository."""
return f"Repository: {owner}/{repo}"
@mcp.prompt()
def review_code(language: str, code: str) -> str:
"""Review a snippet of code."""
return f"Review this {language} code:\n{code}"
@mcp.completion()
async def handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
if isinstance(ref, PromptReference) and argument.name == "language":
return Completion(values=[lang for lang in LANGUAGES if lang.startswith(argument.value)])
return None
- There is one handler per server. Every completion request lands here, and you branch on what's being completed.
- It must be
async def: the SDK awaits it. - It receives three arguments:
ref: which prompt or resource template, as aPromptReferenceor aResourceTemplateReference.isinstanceis how you tell them apart.argument:argument.nameis the argument being completed,argument.valueis what the user has typed so far.context: the arguments already resolved. Ignore it for now.- You return a
Completion(values=[...]), orNonewhen you have nothing to offer.
Tip
argument.value is the prefix the user has typed. The SDK does not filter for you: whatever
you put in values is what the UI shows. The startswith is yours to write.
Try it
Drive it with the in-memory Client, the same one you use in Testing. Call
client.complete() with ref=PromptReference(name="review_code") and
argument={"name": "language", "value": "py"}:
result.completion.values # ['python']
refis the same reference type your handler receives.argumentis a plain dict with exactly two keys,nameandvalue.
Send an empty value and you get the whole list back. lang.startswith("") is true for every language:
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
Ask about code (an argument your handler doesn't recognise) and it returns None, which the SDK turns into an empty list:
result.completion.values # []
None means "no suggestions", never an error. A UI falls back to a plain text box.
A capability you never declared
Registering the handler is the declaration. Connect a client and look:
client.server_capabilities.completions # CompletionsCapability()
You didn't list completions anywhere. The SDK saw the handler and advertised it during the handshake. Every optional capability works this way: the handler is the declaration. (The three primitives are not optional: MCPServer always declares those, handlers or not.)
Check
Go back to the first server.py (the one with no handler) and ask it anyway. The call fails
with a JSON-RPC error:
Method not found
And client.server_capabilities.completions is None. That's the point of the capability: a
well-behaved client checks it and never sends the request you can't answer.
Dependent arguments
github://repos/{owner}/{repo} has two parameters, and the useful values for repo depend on which owner was picked first.
That's what context is for. It carries the arguments the user has already resolved:
from mcp_types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
from mcp.server import MCPServer
mcp = MCPServer("GitHub Explorer")
LANGUAGES = ["go", "javascript", "python", "rust", "typescript"]
REPOS_BY_OWNER = {
"modelcontextprotocol": ["python-sdk", "typescript-sdk", "inspector"],
"pydantic": ["pydantic", "pydantic-ai", "logfire"],
}
@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
"""A GitHub repository."""
return f"Repository: {owner}/{repo}"
@mcp.prompt()
def review_code(language: str, code: str) -> str:
"""Review a snippet of code."""
return f"Review this {language} code:\n{code}"
@mcp.completion()
async def handle_completion(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
if isinstance(ref, PromptReference) and argument.name == "language":
return Completion(values=[lang for lang in LANGUAGES if lang.startswith(argument.value)])
if isinstance(ref, ResourceTemplateReference) and argument.name == "repo":
if context is None or context.arguments is None:
return None
repos = REPOS_BY_OWNER.get(context.arguments.get("owner", ""), [])
return Completion(values=[repo for repo in repos if repo.startswith(argument.value)])
return None
- The new branch fires for the template's
repoparameter. context.argumentsis adict[str, str] | Noneof the values picked so far (here,owner).- No
owneryet means no sensible suggestions, so the handler returnsNone.
The client sends those resolved values with context_arguments=. This time ref is a
ResourceTemplateReference(uri="github://repos/{owner}/{repo}"). Ask for repo with an
empty value and pass context_arguments={"owner": "modelcontextprotocol"}:
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
Drop context_arguments= and the same call returns []. The handler can't know which repos to offer until it knows the owner.
Info
Completion also takes total= and has_more=. Set them when values is a slice of a longer
list, so a UI can show "and 200 more". Most handlers never need them.
Recap
- Completions are suggestions for prompt arguments and resource template parameters. Nothing else.
@mcp.completion()registers the one handler. It'sasync def (ref, argument, context) -> Completion | None.- Branch on
isinstance(ref, ...)and onargument.name. Filter byargument.valueyourself. Nonebecomes an empty list. It is never an error.context.argumentsholds the already-resolved values; the client supplies them ascontext_arguments=.- The
completionscapability appears the moment you register the handler. Without it, the request isMethod not found.
Suggestions help before a tool runs. To ask the user a question in the middle of one, you want Elicitation.