首页 › tutorials › MCP补全原语:参数智能补全

MCP 补全原语:给参数装上自动补全

概述

在 MCP 的世界里,工具(Tools)、资源(Resources)、提示词(Prompts)是大家最熟悉的三类原语,而补全(Completion)是经常被忽视的第四个。它解决一个非常具体的问题:当用户在客户端里填写提示词参数、或拼一个资源模板 URI 时,服务器能不能像 IDE 一样,根据当前输入的半截字符动态给出候选值?本教程带你从协议层到代码层完整实现一个带补全能力的 MCP 服务器,并用官方 SDK 的客户端与 Inspector 双重验证。适合已经写过基础 MCP 服务器、想把"参数体验"做到位的开发者。

前置条件


第一步:把补全放进 MCP 四大原语的地图

很多人写 MCP 服务器只实现了工具,偶尔加几个资源,补全几乎从不碰。补全原语的定位很明确:它不产生给模型用的数据,它只服务"填参数的人"。

flowchart LR
    subgraph 服务端原语
        T[Tools 工具<br/>模型可调用]
        R[Resources 资源<br/>模型可读取]
        P[Prompts 提示词<br/>用户可触发]
        C[Completion 补全<br/>辅助填参]
    end
    User((用户)) -->|填提示词参数<br/>拼资源URI| C
    C -.建议值.-> User
    User -->|触发| P
    User -->|订阅| R
    Model((Claude)) -->|调用| T
    Model -->|读取| R

补全的线上协议非常简单。客户端发出 completion/complete 请求,核心字段只有三个:

字段 含义 取值
ref 正在补全什么 ref/prompt(提示词参数)或 ref/resource(资源模板 URI)
argument 当前在补哪个参数、输入到哪了 { name, value }
context 已填的其它参数(可选) { arguments: { ... } }

服务器返回 completion 对象:values(候选值数组)、可选的 total(全集大小)、可选的 hasMore(是否还有更多)。规范要求 values 不超过 100 条;当候选很多时用 hasMore=true 配合 total 表示"我只给了你一部分"。

常见问题:补全和工具参数的 JSON Schema 枚举(enum)有什么区别?枚举是静态、全量、写死在 Schema 里的,模型和客户端在调用前就看到全部选项;补全是动态、按需、可依赖上下文的,比如"先填 owner=modelcontextprotocol,再补全 repo 时只返回该组织下的仓库"。这是补全真正的价值所在。


第二步:搭一个带提示词和资源模板的服务器骨架

我们先准备一个最小但"可补全"的服务器:一个带参数的提示词 review_code,加一个资源模板 github://repos/{owner}/{repo}。

新建 completion_server.py:

from mcp.server.mcpserver import MCPServer

mcp = MCPServer(name="completion-demo", version="0.1.0")


@mcp.prompt(description="代码评审提示词")
def review_code(language: str, framework: str, code: str) -> str:
    """对给定语言与框架下的代码进行评审。"""
    return (
        f"你是 {framework} 框架的资深 {language} 工程师。"
        f"请评审下面的代码,先指出三个最严重的问题,再给改写建议:\n\n{code}"
    )


@mcp.resource("github://repos/{owner}/{repo}")
def github_repo(owner: str, repo: str) -> str:
    """读取某个 GitHub 仓库的元信息(演示用,返回静态字符串)。"""
    return f"Repository meta: {owner}/{repo}"


if __name__ == "__main__":
    mcp.run(transport="stdio")

操作说明:安装最新版官方 Python SDK,然后以 stdio 方式运行。

uv init mcp-completion-demo && cd mcp-completion-demo
uv add "mcp>=1.2"
uv run completion_server.py

说明:mcp.server.mcpserver.MCPServer 是官方 python-sdk 近期版本提供的高层 API(与 FastMCP 接口一致)。若你的版本暴露的是 from mcp.server.fastmcp import FastMCP,把导入名换成 FastMCP、其余代码不变即可。务必升级到最新版:uv add --upgrade mcp。

预期结果:进程启动后会阻塞在 stdio 循环上,没有任何输出——这是正常的,因为它在等 JSON-RPC 消息。此时服务器已经具备 prompts/list 和 resources/templates/list 的能力,但还没有补全能力。下一步补上。

常见问题:报 ImportError: cannot import name 'MCPServer'——说明 SDK 版本过旧,执行 uv add --upgrade mcp 升级到 1.2 以上。


第三步:实现 @mcp.completion() 补全处理器

补全处理器是一个统一的入口:所有提示词参数、所有资源模板参数的补全请求都从这里进来,你需要用 ref 和 argument.name 做分发。

from mcp.types import (
    Completion,
    CompletionArgument,
    CompletionContext,
    PromptReference,
    ResourceTemplateReference,
)


@mcp.completion()
async def handle_completion(
    ref: PromptReference | ResourceTemplateReference,
    argument: CompletionArgument,
    context: CompletionContext | None,
) -> Completion | None:
    """统一处理提示词与资源模板的参数补全。"""

    # —— 1. 提示词参数补全 ——
    if isinstance(ref, PromptReference):
        # 语言参数:静态候选 + 前缀过滤
        if ref.name == "review_code" and argument.name == "language":
            langs = ["python", "typescript", "javascript", "go", "rust", "java"]
            return Completion(
                values=[x for x in langs if x.startswith(argument.value)],
                has_more=False,
            )
        # 框架参数:依赖已填的 language 做上下文补全
        if ref.name == "review_code" and argument.name == "framework":
            chosen = (context.arguments or {}).get("language", "") if context else ""
            by_lang = {
                "python": ["fastapi", "django", "flask", "langchain"],
                "typescript": ["next", "nest", "express", "remix"],
                "rust": ["axum", "actix", "tokio"],
            }
            pool = by_lang.get(chosen, ["fastapi", "next", "django"])
            return Completion(
                values=[x for x in pool if x.startswith(argument.value)],
                has_more=False,
            )

    # —— 2. 资源模板参数补全 ——
    if isinstance(ref, ResourceTemplateReference):
        if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "owner":
            orgs = ["modelcontextprotocol", "anthropics", "vercel", "huggingface"]
            return Completion(values=[o for o in orgs if o.startswith(argument.value)])
        if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
            # 依赖已填的 owner,给出该组织下的仓库候选
            owner = (context.arguments or {}).get("owner", "") if context else ""
            if owner == "modelcontextprotocol":
                repos = ["python-sdk", "typescript-sdk", "specification", "kotlin-sdk"]
                return Completion(values=[r for r in repos if r.startswith(argument.value)])

    # 不认识的 ref,返回 None,客户端会安静地不弹候选
    return None

操作说明:把这段代码接在上一步文件末尾、mcp.run() 之前。

预期结果:服务器现在声明了补全能力。当客户端对 review_code 的 language 参数发来 value="py" 时,处理器会返回 values=["python"];当 value="" 时返回全部 6 种语言。

常见问题与解决方案:

  1. "补全处理器没被调用"——九成是 ref 的类型判断写错了。提示词用 PromptReference(看 ref.name),资源模板用 ResourceTemplateReference(看 ref.uri,注意是带 {owner}/{repo} 的模板字符串,不是已替换的实例 URI)。务必用 isinstance 而不是字符串相等去判 ref。
  2. "返回了候选但客户端不显示"——补全是建议而非命令,客户端有权决定是否、如何展示。某些客户端只在用户主动按 Tab / 触发补全时才发请求,不会在你每次敲键时都问一遍。这属于协议设计,不是你的 bug。
  3. "想返回的值太多"——别一次性塞上千条。规范要求 values ≤ 100;超过就用 has_more=True 并把真实总数放进 total,让客户端决定是否再问一次。

第四步:写一个最小客户端,亲手发起 complete 请求

光在服务器里写处理器,看不见摸不着。我们用官方 SDK 写一个最小客户端,把整条 complete 链路跑通,这是验证补全是否生效最可靠的方式。

新建 completion_client.py:

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.types import PromptReference, ResourceTemplateReference

server_params = StdioServerParameters(
    command="uv",
    args=["run", "completion_server.py"],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # 1) 提示词参数补全:language 前缀 "py"
            r = await session.complete(
                ref=PromptReference(type="ref/prompt", name="review_code"),
                argument={"name": "language", "value": "py"},
            )
            print("language 候选:", r.completion.values)   # ['python']

            # 2) 上下文感知补全:已知 language=python,补 framework
            r = await session.complete(
                ref=PromptReference(type="ref/prompt", name="review_code"),
                argument={"name": "framework", "value": ""},
                context_arguments={"language": "python"},
            )
            print("framework 候选:", r.completion.values)
            # ['fastapi', 'django', 'flask', 'langchain']

            # 3) 资源模板参数补全:owner 前缀 "model"
            r = await session.complete(
                ref=ResourceTemplateReference(
                    type="ref/resource", uri="github://repos/{owner}/{repo}"
                ),
                argument={"name": "owner", "value": "model"},
            )
            print("owner 候选:", r.completion.values)      # ['modelcontextprotocol']

            # 4) 二级上下文补全:已知 owner,补 repo
            r = await session.complete(
                ref=ResourceTemplateReference(
                    type="ref/resource", uri="github://repos/{owner}/{repo}"
                ),
                argument={"name": "repo", "value": ""},
                context_arguments={"owner": "modelcontextprotocol"},
            )
            print("repo 候选:", r.completion.values)
            # ['python-sdk', 'typescript-sdk', 'specification', 'kotlin-sdk']


if __name__ == "__main__":
    asyncio.run(main())

操作说明:

uv add "mcp[cli]>=1.2"
uv run completion_client.py

预期结果(依次打印):

language 候选: ['python']
framework 候选: ['fastapi', 'django', 'flask', 'langchain']
owner 候选: ['modelcontextprotocol']
repo 候选: ['python-sdk', 'typescript-sdk', 'specification', 'kotlin-sdk']

这四行输出就是补全原语全部能力的浓缩:前缀过滤、上下文感知、资源模板补全、多级联动。

常见问题:客户端报 capability not supported——检查服务器是否真的把 @mcp.completion() 注册成功。可以在客户端 initialize() 后打印 session.server_capabilities,确认里面有补全相关标记(不同 SDK 版本字段名略有差异,关键是你的处理器已挂载)。


第五步:用 MCP Inspector 可视化验证

写客户端验证可靠但繁琐。MCP Inspector 是官方的可视化调试器,它原生支持触发补全,是日常开发的首选。

操作说明:

npx @modelcontextprotocol/inspector uv run completion_server.py

浏览器打开终端提示的本地地址(通常是 http://localhost:6274)。

操作步骤与预期结果:

  1. 在 Tools 面板:此处为空(我们没定义工具,符合预期)。
  2. 切到 Prompts 面板:能看到 review_code。点开它,会出现 language、framework、code 三个输入框。
  3. 把光标放进 language 输入框,输入 t——Inspector 会向服务器发 completion/complete,下方或下拉区出现 typescript 候选。
  4. 先在 language 填 python,再把光标放进 framework 输入框——此时上下文已携带 language=python,候选变为 fastapi/django/flask/langchain。
  5. 切到 Resources → Resource Templates:看到 github://repos/{owner}/{repo}。填 owner 触发补全,再填 repo 时候选随 owner 联动。

常见问题:Inspector 里输入框不弹候选——确认左下角连接状态是 Connected,并在 Server Capabilities 区确认补全能力已声明。若能力区看不到补全项,回到服务器检查 @mcp.completion() 是否在 run() 之前注册。


第六步:把补全接到真实数据源(动态候选)

静态列表只能演示语法。真实场景里,候选往往来自数据库、文件系统或远程 API。关键原则是:补全处理器可以是 async,里面能做任何 I/O,但必须快——客户端期望它在几百毫秒内返回,否则用户体验会卡顿。

下面把"语言候选"接成一个从本地 SUPPORTED.md 动态读取的版本:

import pathlib

SUPPORTED_FILE = pathlib.Path(__file__).parent / "SUPPORTED.md"


async def _load_languages() -> list[str]:
    """从文件读取支持的语言列表,每行一个。"""
    if not SUPPORTED_FILE.exists():
        return ["python", "typescript"]
    lines = [ln.strip().lstrip("- ").strip() for ln in SUPPORTED_FILE.read_text().splitlines()]
    return [ln for ln in lines if ln and not ln.startswith("#")]


@mcp.completion()
async def handle_completion(ref, argument, context):
    if isinstance(ref, PromptReference) and ref.name == "review_code":
        if argument.name == "language":
            pool = await _load_languages()          # 动态来源
            matches = [x for x in pool if x.startswith(argument.value)]
            return Completion(
                values=matches[:100],               # 硬上限 100
                total=len(matches),
                has_more=len(matches) > 100,
            )
    return None

预期结果:改 SUPPORTED.md 的内容后无需重启即可生效(每次补全重新读文件);当文件里有超过 100 行时,has_more=True、total 反映真实总数。

常见问题:补全很慢——别在处理器里做重查询。补全是高频、低延迟调用,远程调用务必加超时与缓存,复杂筛选交给工具去做,补全只负责"给个候选下拉"。


实战案例:时区与国家代码补全服务器

把上面所有片段串成一个可运行的小项目 tz-completion:一个 convert_time 提示词,参数 timezone 可补全;一个 country://{code}/info 资源模板,参数 code 可补全。

# tz_completion_server.py
from mcp.server.mcpserver import MCPServer
from mcp.types import (
    Completion, CompletionArgument, CompletionContext,
    PromptReference, ResourceTemplateReference,
)

mcp = MCPServer(name="tz-completion", version="0.1.0")

TIMEZONES = [
    "Asia/Shanghai", "Asia/Tokyo", "Asia/Singapore",
    "Europe/London", "Europe/Paris", "Europe/Berlin",
    "America/New_York", "America/Los_Angeles", "UTC",
]
COUNTRIES = {"CN": "China", "JP": "Japan", "US": "United States",
             "GB": "United Kingdom", "DE": "Germany", "FR": "France"}


@mcp.prompt(description="时区换算提示词")
def convert_time(timezone: str, source_time: str) -> str:
    return f"请把时间 {source_time} 换算到 {timezone} 时区,给出当天各主要城市的对应时刻。"


@mcp.resource("country://{code}/info")
def country_info(code: str) -> str:
    name = COUNTRIES.get(code.upper(), "Unknown")
    return f"国家代码 {code.upper()} -> {name}"


@mcp.completion()
async def handle_completion(ref, argument: CompletionArgument, context):
    if isinstance(ref, PromptReference) and ref.name == "convert_time" \
            and argument.name == "timezone":
        v = argument.value.lower()
        return Completion(values=[tz for tz in TIMEZONES if tz.lower().startswith(v)])

    if isinstance(ref, ResourceTemplateReference) \
            and ref.uri == "country://{code}/info" and argument.name == "code":
        v = argument.value.upper()
        return Completion(values=[c for c in COUNTRIES if c.startswith(v)])

    return None


if __name__ == "__main__":
    mcp.run(transport="stdio")

用 Inspector 连上后:在 convert_time 的 timezone 框里敲 asia/,立刻看到 Asia/Shanghai、Asia/Tokyo、Asia/Singapore;在资源模板 country://{code}/info 里敲 code=u,得到 US、GB(前缀匹配,大小写归一)。这就是一个完整的、带动态参数补全的 MCP 服务器。

sequenceDiagram
    participant U as 用户(客户端UI)
    participant C as MCP Client
    participant S as 补全服务器

    U->>C: 在 timezone 框输入 "asia/"
    C->>S: completion/complete<br/>{ref: convert_time, argument:{timezone:"asia/"}}
    S-->>C: {values:["Asia/Shanghai","Asia/Tokyo","Asia/Singapore"]}
    C-->>U: 下拉显示 3 个候选

    U->>C: 选定 timezone=Asia/Shanghai,继续填 source_time
    Note over C,S: 补全只服务"填参",不参与模型推理
    U->>C: 触发提示词
    C->>S: prompts/get(name=convert_time, args=...)
    S-->>C: 渲染后的消息

关于客户端支持的现实提醒

补全原语的价值完全取决于客户端是否消费它。当前生态里:

所以一个务实的工程结论是:把补全实现成"锦上添花"而非"必需路径"。哪怕客户端不消费,你的服务器依然正常工作(提示词照常渲染、资源照常返回);一旦接上支持补全的客户端,体验立刻拉满。这让它成为一个"低风险、高回报"的原语——实现成本不高,但在支持它的客户端里能显著降低用户拼错参数的概率。


延伸阅读

参考来源

  1. Completion — Model Context Protocol(官方规范) — completion/complete 的协议定义、用户交互模型与 hasMore/total 分页语义
  2. python-sdk completion_client.py(官方示例) — 用 session.complete() 发起 ref/prompt 与 ref/resource 补全的权威客户端代码
  3. python-sdk 仓库与 README — @mcp.completion() 高层 API、Completions 小节与版本说明
  4. Schema Validation and Completion — TypeScript SDK(DeepWiki) — TS 端补全请求处理器的实现思路与协议合规校验
  5. Resource and Prompt Management — MCP Inspector(DeepWiki) — 在 Inspector 中可视化触发提示词/资源模板参数补全的操作流程

训练营适用性评估

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入