补全
在你的服务器之上构建 UI 的客户端,会想在用户输入时自动补全参数值:语言名称、仓库名称、文件路径。
补全(completion)就是服务器提供这些建议的方式。
值得补全的东西
补全只适用于两样东西:提示词的参数和资源模板的参数。所以先写一个两者各有一个的服务器:
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() 装饰的函数:
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
- 每个服务器只有一个处理函数。所有补全请求都会落到这里,由你根据正在补全的对象分支处理。
- 它必须是
async def:SDK 会 await 它。 - 它接收三个参数:
ref:是哪一个提示词或资源模板,类型为PromptReference或ResourceTemplateReference。用isinstance区分两者。argument:argument.name是正在补全的参数,argument.value是用户目前已输入的内容。context:已经确定的参数。暂时忽略它。- 返回一个
Completion(values=[...]);没有可提供的建议时返回None。
Tip
argument.value 是用户已输入的前缀。SDK 不会替你过滤:放进 values 的是什么,UI 显示的就是什么。startswith 得你自己写。
试一试
用 测试 中的内存 Client 来驱动它。调用 client.complete(),传入 ref=PromptReference(name="review_code") 和 argument={"name": "language", "value": "py"}:
result.completion.values # ['python']
ref与处理函数收到的引用类型相同。argument是一个普通的 dict,只有name和value两个键。
发送空的 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 看到处理函数,就替你声明了这项能力。每一项可选能力都是这样:处理函数就是声明。(三种原语不是可选的:无论有没有处理函数,MCPServer 总会声明它们。)
Check
回到第一个 server.py(没有处理函数的那个),照样向它发请求。调用会失败,并返回一个 JSON-RPC 错误:
Method not found
而且 client.server_capabilities.completions 是 None。这正是能力的意义所在:行为规范的客户端会先检查它,绝不会发出你无法响应的请求。
有依赖关系的参数
github://repos/{owner}/{repo} 有两个参数,而 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是dict[str, str] | None,保存目前已选定的值(这里是owner)。- 还没有
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=,同样的调用会返回 []。不知道 owner,处理函数就无从知道该提供哪些仓库。
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)。工具除了文本还能返回什么,见 图像、音频和图标。