補完
サーバーの上に UI を構築するクライアントは、ユーザーの入力に合わせて引数の値を自動補完したいと考えます。言語名、リポジトリ名、ファイルパスなどです。
補完(completions)は、サーバーがそうした候補を提供するための仕組みです。
補完する対象を用意する
補完が適用されるのはちょうど 2 つだけです。プロンプトの引数と、リソーステンプレートのパラメーターです。そこで、まずはその両方を 1 つずつ持つサーバーから始めます。
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}"
ここにはまだ補完に関するものは何もありません。
review_codeはlanguageを受け取ります。どの綴りが受け付けられるかをユーザーに推測させるべきではありません。github_repoはownerとrepoを受け取ります。両方とも自由入力のテキストボックスでは、使いにくいフォームになります。
補完ハンドラー
@mcp.completion() でデコレートした関数を 1 つ追加します。
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
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
- ハンドラーはサーバーごとに 1 つです。補完リクエストはすべてここに届くので、何が補完されているかに応じて分岐します。
async defでなければなりません。SDK がこれを await します。- 3 つの引数を受け取ります。
ref:「どの」プロンプトまたはリソーステンプレートかを表し、PromptReferenceかResourceTemplateReferenceのどちらかです。見分けるにはisinstanceを使います。argument:argument.nameは補完対象の引数、argument.valueはユーザーがこれまでに入力した文字列です。context:すでに解決済みの引数です。今は無視してかまいません。- 戻り値は
Completion(values=[...])、または提示するものがないときはNoneです。
Tip
argument.value はユーザーが入力したプレフィックスです。SDK はフィルタリングをしません。values に入れたものがそのまま UI に表示されます。startswith は自分で書きます。
試してみる
テストで紹介したインメモリの Client で動かします。ref=PromptReference(name="review_code") と argument={"name": "language", "value": "py"} を指定して client.complete() を呼び出します。
result.completion.values # ['python']
refはハンドラーが受け取るのと同じ参照型です。argumentはnameとvalueのちょうど 2 つのキーを持つ、普通の dict です。
空の value を送ると、リスト全体が返ってきます。lang.startswith("") はどの言語に対しても真だからです。
result.completion.values # ['go', 'javascript', 'python', 'rust', 'typescript']
code(ハンドラーが認識しない引数)について尋ねると None が返り、SDK はそれを空のリストに変換します。
result.completion.values # []
None は「候補なし」という意味であり、決してエラーではありません。UI は普通のテキストボックスにフォールバックします。
宣言した覚えのないケイパビリティ
ハンドラーを登録すること自体が宣言です。クライアントを接続して確認してみてください。
client.server_capabilities.completions # CompletionsCapability()
completions をどこにも列挙していません。SDK がハンドラーを見つけて、代わりにケイパビリティを宣言したのです。「オプション」のケイパビリティはすべてこの仕組みで動きます。ハンドラーが宣言そのものです。(3 つのプリミティブはオプションではありません。MCPServer はハンドラーの有無にかかわらず常にそれらを宣言します。)
Check
最初の server.py(ハンドラーのないほう)に戻り、それでも問い合わせてみてください。呼び出しは JSON-RPC エラーで失敗します。
Method not found
そして client.server_capabilities.completions は None です。これこそがケイパビリティの存在意義です。行儀のよいクライアントはこれを確認し、応答できないリクエストは最初から送りません。
依存する引数
github://repos/{owner}/{repo} にはパラメーターが 2 つあり、repo として意味のある値は、先にどの owner が選ばれたかによって変わります。
そのためにあるのが context です。ユーザーがすでに解決した引数を運びます。
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
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
- 新しい分岐は、テンプレートの
repoパラメーターに対して実行されます。 context.argumentsは、これまでに選ばれた値(ここではowner)を持つdict[str, str] | Noneです。ownerがまだなければ意味のある候補も出せないので、ハンドラーはNoneを返します。
クライアントは、解決済みの値を context_arguments= で送ります。今回の ref は ResourceTemplateReference(uri="github://repos/{owner}/{repo}") です。空の value で repo を要求し、context_arguments={"owner": "modelcontextprotocol"} を渡します。
result.completion.values # ['python-sdk', 'typescript-sdk', 'inspector']
context_arguments= を外すと、同じ呼び出しが [] を返します。ハンドラーは、オーナーがわかるまでどのリポジトリを提示すべきか知りようがありません。
Info
Completion は total= と has_more= も受け取ります。values がより長いリストの一部であるときに設定すると、UI が「ほか 200 件」のように表示できます。ほとんどのハンドラーには必要ありません。
まとめ
- 補完は、プロンプトの引数とリソーステンプレートのパラメーターに対する候補です。それ以外にはありません。
@mcp.completion()で唯一のハンドラーを登録します。シグネチャはasync def (ref, argument, context) -> Completion | Noneです。isinstance(ref, ...)とargument.nameで分岐します。argument.valueによるフィルタリングは自分で行います。Noneは空のリストになります。決してエラーではありません。context.argumentsは解決済みの値を保持し、クライアントはそれをcontext_arguments=として渡します。completionsケイパビリティは、ハンドラーを登録した瞬間に現れます。ハンドラーがなければ、リクエストはMethod not foundになります。
候補が役立つのは、ユーザーがまだプロンプトやテンプレートを「入力している」あいだです。ツール呼び出しの「途中」でユーザーに質問したいなら、必要なのはエリシテーション(elicitation)です。ツールがテキスト以外に返せるものはすべて画像、音声、アイコンにまとめてあります。