<!-- title: MCP补全原语：参数智能补全 -->

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

## 概述

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

## 前置条件

- 已掌握任一 MCP 服务器的"Hello World"（看过本系列的 Python FastMCP 或 TS SDK 教程即可）
- Python 3.10+，已安装 `uv`（推荐）或 `pip`
- 能在终端运行 `npx`（用于 MCP Inspector）
- 了解 JSON-RPC 请求/响应的基本结构

---

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

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

```mermaid
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`：

```python
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 方式运行。

```bash
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` 做分发。

```python
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`：

```python
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())
```

**操作说明**：

```bash
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 是官方的可视化调试器，它原生支持触发补全，是日常开发的首选。

**操作说明**：

```bash
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` 动态读取的版本：

```python
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` 可补全。

```python
# 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 服务器。

```mermaid
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: 渲染后的消息
```

---

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

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

- **MCP Inspector**：一等公民，是最可靠的验证工具，强烈建议把它作为开发期标配。
- **自定义客户端 / IDE 类客户端**：补全是这类客户端的强项，体验接近 IDE 代码补全。
- **命令行 AI 客户端**：对补全的消费程度不一，部分客户端不会主动发 `completion/complete`。

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

---

## 延伸阅读

- 官方补全规范（2025-06-18 / 2025-11-25）：`completion/complete` 的请求与响应字段定义、`hasMore`/`total` 的分页语义。
- 官方 python-sdk 的 `examples/snippets/servers/completion.py` 与 `clients/completion_client.py`：本教程代码的权威出处。
- 官方 python-sdk README 的 *Completions* 小节：高层 API 的补全用法与版本说明。
- TypeScript SDK 的 Schema Validation & Completion 文档：如果你用 TS 实现，补全通过注册 `CompletionRequest` 处理器返回 `CompletionResult`，思路与 Python 完全一致。
- MCP Inspector 的 Resource & Prompt Management 文档：可视化触发补全的操作细节。

## 参考来源

1. [Completion — Model Context Protocol（官方规范）](https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/completion) — `completion/complete` 的协议定义、用户交互模型与 `hasMore`/`total` 分页语义
2. [python-sdk completion_client.py（官方示例）](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/completion_client.py) — 用 `session.complete()` 发起 `ref/prompt` 与 `ref/resource` 补全的权威客户端代码
3. [python-sdk 仓库与 README](https://github.com/modelcontextprotocol/python-sdk) — `@mcp.completion()` 高层 API、Completions 小节与版本说明
4. [Schema Validation and Completion — TypeScript SDK（DeepWiki）](https://deepwiki.com/modelcontextprotocol/typescript-sdk/7.2-schema-validation-and-completion) — TS 端补全请求处理器的实现思路与协议合规校验
5. [Resource and Prompt Management — MCP Inspector（DeepWiki）](https://deepwiki.com/modelcontextprotocol/inspector/3.4-resource-and-prompt-management) — 在 Inspector 中可视化触发提示词/资源模板参数补全的操作流程

## 训练营适用性评估

- **适合的训练营类型**：MCP 进阶开发营、AI 工具链/Agent 平台工程营。不适合纯入门营——学员需先写过至少一个能跑的 MCP 服务器。
- **建议教学时长**：1 个半天（约 3 小时）。其中 30 分钟讲协议与四大原语定位，90 分钟动手实现服务器+客户端，60 分钟用 Inspector 调试与扩展动态数据源。
- **可裁剪/加深的点**：若时长紧张，可跳过第六步（动态数据源）与实战案例，保留到第三、四步即可让学员完整体验补全闭环；若想加深，可让学员把补全接到一个真实数据库或内部 API，并加上缓存与超时，作为结业作业。

> [!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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
