Skip to content

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:

server.py
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_code takes a language. A user shouldn't have to guess which spellings you accept.
  • github_repo takes an owner and a repo. Free-text boxes for both make a bad form.

The completion handler

Add one function decorated with @mcp.completion():

server.py
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 a PromptReference or a ResourceTemplateReference. isinstance is how you tell them apart.
  • argument: argument.name is the argument being completed, argument.value is what the user has typed so far.
  • context: the arguments already resolved. Ignore it for now.
  • You return a Completion(values=[...]), or None when 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']
  • ref is the same reference type your handler receives.
  • argument is a plain dict with exactly two keys, name and value.

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:

server.py
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 repo parameter.
  • context.arguments is a dict[str, str] | None of the values picked so far (here, owner).
  • No owner yet means no sensible suggestions, so the handler returns None.

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's async def (ref, argument, context) -> Completion | None.
  • Branch on isinstance(ref, ...) and on argument.name. Filter by argument.value yourself.
  • None becomes an empty list. It is never an error.
  • context.arguments holds the already-resolved values; the client supplies them as context_arguments=.
  • The completions capability appears the moment you register the handler. Without it, the request is Method not found.

Suggestions help before a tool runs. To ask the user a question in the middle of one, you want Elicitation.