跳转至

补全

机器翻译

本页由英文文档自动翻译而来,以英文页面为准。如果有读起来不对的地方,翻译页面说明了如何反馈。

在你的服务器之上构建 UI 的客户端,会想在用户输入时自动补全参数值:语言名称、仓库名称、文件路径。

补全(completion)就是服务器提供这些建议的方式。

值得补全的东西

补全只适用于两样东西:提示词的参数和资源模板的参数。所以先写一个两者各有一个的服务器:

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}"

这里还没有任何与补全相关的内容。

  • review_code 接受一个 language。用户不该靠猜来知道你接受哪些写法。
  • github_repo 接受 ownerrepo。两个都用自由文本框,这个表单会很难用。

补全处理函数

添加一个@mcp.completion() 装饰的函数:

server.py
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:是哪一个提示词或资源模板,类型为 PromptReferenceResourceTemplateReference。用 isinstance 区分两者。
  • argumentargument.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,只有 namevalue 两个键。

发送空的 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.completionsNone。这正是能力的意义所在:行为规范的客户端会先检查它,绝不会发出你无法响应的请求。

有依赖关系的参数

github://repos/{owner}/{repo} 有两个参数,而 repo 的有用取值取决于先选了哪个 owner

这就是 context 的用处。它携带用户已经确定的参数:

server.py
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.argumentsdict[str, str] | None,保存目前已选定的值(这里是 owner)。
  • 还没有 owner,就没有合理的建议可给,所以处理函数返回 None

客户端通过 context_arguments= 发送这些已确定的值。这次 refResourceTemplateReference(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)。工具除了文本还能返回什么,见 图像、音频和图标