《MCP 协议设计与实现》完整目录
- 前言
- 第1章 为什么需要 MCP
- 第02章 架构总览:Host-Client-Server 模型
- 第03章 JSON-RPC 与消息格式
- 第04章 生命周期与能力协商
- 第05章 Tool:让 Agent 调用世界
- 第6章 Resource:结构化的上下文注入
- 第7章 Prompt:可复用的交互模板
- 第8章 TypeScript Server 实现剖析
- 第09章 TypeScript Client 实现剖析
- 第10章 Python Server 实现剖析
- 第11章 Python Client 实现剖析
- 第12章 STDIO 传输:本地进程通信
- 第13章 Streamable HTTP:远程流式传输
- 第14章 SSE 与 WebSocket
- 第15章 OAuth 2.1 认证框架
- 第16章 服务发现与客户端注册
- 第17章 sampling
- 第18章 Elicitation、Roots 与配置管理(当前)
- 第19章 Claude Code 的 MCP 客户端:12 万行的实战
- 第20章 从零构建一个生产级 MCP Server
- 第21章 设计模式与架构决策
第18章 Elicitation、Roots 与配置管理
"A good protocol doesn't just let machines talk to machines --- it lets machines ask people the right questions at the right time."
:::tip 本章要点
- 理解 Elicitation 的两种模式:Form Mode(结构化表单)与 URL Mode(带外交互)
- 掌握 Roots 机制如何让 Server 感知 Client 的文件系统边界
- 学会使用 Completion 为 Prompt 和 Resource 参数提供自动补全
- 理解 Progress、Cancellation、Logging 三大操作生命周期工具
- 认识这些特性如何与能力协商机制深度配合 :::
18.1 被忽视的反向通道
在前面的章节中,我们已经深入分析了 MCP 的三大核心原语------Tools、Resources、Prompts,以及 Sampling 这一让 Server 借助 Client 的 LLM 能力的机制。但如果你仔细回顾这些交互,会发现一个共同的模式:大多数请求都是 Client 发起、Server 响应的。
现实世界的交互远比这复杂。一个 Server 在处理工具调用时,可能需要用户的额外输入------比如 GitHub 用户名、部署环境选择、或者第三方 API 密钥。它也可能需要知道 Client 当前工作在哪些目录下,以便缩小搜索范围。这些需求催生了 MCP 协议中一组精巧的"反向通道"机制。
本章将覆盖六个协议特性,它们共同构成了 MCP 的配置管理与操作生命周期层:
向用户提问"] R["Roots
感知文件边界"] S["Sampling
借用 LLM(第17章)"] end subgraph "操作生命周期" C["Completion
参数自动补全"] P["Progress
进度追踪"] CA["Cancellation
请求取消"] L["Logging
结构化日志"] end E --> |"嵌套在"| TK["tools/call 等请求内"] R --> |"嵌套在"| TK S --> |"嵌套在"| TK P --> |"附着于"| TK CA --> |"终止"| TK style E fill:#ec4899,color:#fff,stroke:none style R fill:#8b5cf6,color:#fff,stroke:none style S fill:#6366f1,color:#fff,stroke:none style C fill:#3b82f6,color:#fff,stroke:none style P fill:#10b981,color:#fff,stroke:none style CA fill:#f59e0b,color:#fff,stroke:none style L fill:#64748b,color:#fff,stroke:none
18.2 Elicitation:Server 向用户提问
18.2.1 为什么需要 Elicitation
考虑一个场景:你的 MCP Server 集成了 Jira,用户让 AI "创建一个 Bug 工单"。Server 需要知道项目名称、优先级、指派人------这些信息并不在用户的原始请求中。在没有 Elicitation 之前,Server 只能返回错误提示,让用户重新措辞,体验极差。
Elicitation 解决的核心问题是:在一次工具调用的执行过程中,Server 可以"暂停",向用户收集额外信息,然后继续执行。这创造了一种嵌套的交互模式,极大地扩展了工具的表达能力。
18.2.2 两种模式的设计哲学
MCP 的 Elicitation 提供两种截然不同的模式,源于一个根本性的安全考量:敏感信息绝不能经过 MCP Client 和 LLM 上下文。
| 特性 | Form Mode(表单模式) | URL Mode(URL 模式) |
|---|---|---|
| 数据流向 | 用户 → Client → Server | 用户 → 浏览器 → Server(绕过 Client) |
| 适用场景 | 用户名、偏好设置、枚举选择 | 密码、API 密钥、OAuth 授权、支付 |
| Schema 支持 | JSON Schema 子集 | 无(交互在外部页面完成) |
| Client 可见数据 | 全部可见 | 仅 URL 本身 |
| 安全级别 | 常规 | 高敏感 |
这种划分体现了 MCP 协议的一个核心原则:安全不是可选项,而是架构决策。Form Mode 用于日常数据收集,URL Mode 则为敏感操作提供了完整的安全隔离。
18.2.3 能力声明
Elicitation 的使用必须通过能力协商。Client 在初始化时声明支持的模式:
json
{
"capabilities": {
"elicitation": {
"form": {},
"url": {}
}
}
}
向后兼容规则是:空的 elicitation 对象等价于仅声明 form 模式。Server 必须检查 Client 声明的能力,绝不能发送 Client 不支持的模式的请求。
18.2.4 Form Mode 详解
Form Mode 的请求通过 elicitation/create 方法发送,包含一个 requestedSchema 字段,定义了期望收集的数据结构:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "请提供你的联系信息",
"requestedSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"title": "姓名",
"description": "你的全名"
},
"email": {
"type": "string",
"format": "email",
"title": "邮箱"
},
"role": {
"type": "string",
"title": "角色",
"oneOf": [
{ "const": "dev", "title": "开发者" },
{ "const": "pm", "title": "产品经理" },
{ "const": "design", "title": "设计师" }
]
}
},
"required": ["name", "email"]
}
}
}
Schema 被刻意限制为扁平的原始类型对象------不支持嵌套对象、不支持对象数组(枚举数组除外)。这不是技术限制,而是有意为之的设计决策:简化 Client 的 UI 渲染负担,因为每个 Client 都需要将这个 Schema 转换为可交互的表单界面。
支持的类型包括:
- string :支持
minLength、maxLength、pattern、format(email/uri/date/date-time) - number / integer :支持
minimum、maximum - boolean :带可选
default - enum :通过
enum数组或oneOf常量实现单选,通过array+items.enum实现多选
所有类型都支持 default 值,Client 应当用默认值预填充表单。
18.2.5 三态响应模型
用户面对 Elicitation 请求时有三种选择,这是协议层面的明确区分:
json
// Accept --- 用户提交了数据
{ "action": "accept", "content": { "name": "张三", "email": "z@example.com" } }
// Decline --- 用户明确拒绝
{ "action": "decline" }
// Cancel --- 用户关闭了对话框(未做选择)
{ "action": "cancel" }
三态而非二态的设计反映了真实的用户意图光谱:Accept 意味着"我愿意且数据在这里",Decline 意味着"我看到了请求但不愿意",Cancel 意味着"我还没做决定就离开了"。Server 应当针对每种状态做不同处理------Decline 时可以提供替代方案,Cancel 时可以稍后再次询问。
18.2.6 URL Mode 与安全边界
当需要收集 API 密钥、处理 OAuth 授权、或完成支付流程时,数据绝不能经过 MCP Client。URL Mode 的流程完全绕过了 Client 的数据通道:
URL Mode 的安全规范极其严格:
- Server 不得在 URL 中包含用户敏感信息
- Server 不得提供预认证的 URL
- Client 必须显示完整 URL 并获得用户明确同意后才能打开
- Client 必须使用安全的浏览器环境(如 iOS 的 SFSafariViewController),而非可被宿主应用窥探的 WebView
- Server 必须验证打开 URL 的用户与发起 Elicitation 的用户是同一人(防止钓鱼攻击)
18.2.7 URLElicitationRequiredError
有时候 Server 在收到 tools/call 时才发现需要外部授权。协议为此定义了错误码 -32042(URLElicitationRequiredError),Server 可以在响应中返回所需的 Elicitation 列表:
json
{
"error": {
"code": -32042,
"message": "需要授权才能继续",
"data": {
"elicitations": [
{
"mode": "url",
"elicitationId": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://mcp.example.com/connect?id=550e8400",
"message": "需要授权访问你的 GitHub 仓库"
}
]
}
}
}
Client 收到这个错误后,可以引导用户完成授权,然后重试原始请求。这种"失败-引导-重试"的模式让工具调用具备了自愈能力。
18.2.8 SDK 中的实现
在 TypeScript SDK 中,Server 通过 server.elicitInput() 方法发起 Elicitation:
typescript
mcpServer.registerTool(
'register_user',
{ description: '注册新用户' },
async () => {
const result = await mcpServer.server.elicitInput({
mode: 'form',
message: '请提供注册信息:',
requestedSchema: {
type: 'object',
properties: {
username: { type: 'string', title: '用户名', minLength: 3 },
email: { type: 'string', format: 'email', title: '邮箱' }
},
required: ['username', 'email']
}
});
if (result.action === 'accept' && result.content) {
return {
content: [{ type: 'text', text: `注册成功:${result.content.username}` }]
};
}
return { content: [{ type: 'text', text: '用户取消了注册' }] };
}
);
注意 elicitInput 是一个异步调用------它会阻塞当前工具执行,直到用户做出响应。在 Server 端,这意味着底层需要维护请求的状态,并在收到 Client 的响应后恢复执行。
18.2.9 请求关联约束
一个至关重要的协议约束:所有 Server → Client 的请求(包括 Elicitation、Roots、Sampling)必须关联到一个正在进行的 Client → Server 的请求 。换言之,Server 不能"无缘无故"地向用户提问------它只能在处理 tools/call、resources/read、prompts/get 等请求的过程中发起 Elicitation。
这个约束不仅是语义上的合理性要求,更是面向未来传输层设计的考量------某些传输实现可能不支持独立的 Server 发起请求。
18.3 Roots:文件系统边界感知
18.3.1 什么是 Roots
Roots 是 MCP 中一个简洁但重要的概念:Client 告诉 Server "我关心哪些目录"。
json
{
"roots": [
{
"uri": "file:///home/user/projects/frontend",
"name": "Frontend Repository"
},
{
"uri": "file:///home/user/projects/backend",
"name": "Backend Repository"
}
]
}
Roots 是信息性的引导而非访问控制------协议并不强制 Server 只能在 Roots 范围内操作。但一个行为良好的 Server 应当尊重这些边界,将搜索、分析等操作限定在指定的目录内。
18.3.2 动态变更通知
Roots 不是静态的。当用户在 IDE 中打开新项目、关闭工作空间时,Client 应该发送变更通知:
json
{
"jsonrpc": "2.0",
"method": "notifications/roots/list_changed"
}
Server 收到通知后,通过 roots/list 重新查询当前的 Roots 列表。这种"通知 + 拉取"的模式在 MCP 中随处可见(Tools、Resources、Prompts 的列表变更都用相同模式),体现了协议设计的一致性。
18.3.3 能力声明与安全
Client 通过 roots 能力声明支持,listChanged 子字段表示是否会发送变更通知:
json
{
"capabilities": {
"roots": {
"listChanged": true
}
}
}
安全方面,Client 必须在暴露 Roots 前获得用户同意,必须验证 Root URI 防止路径穿越,必须实现适当的访问控制。Server 则应当缓存 Root 信息、在 Roots 不可用时优雅降级。
18.4 Completion:参数自动补全
18.4.1 提升用户输入体验
当用户在 Client UI 中填写 Prompt 参数或 Resource 模板的 URI 时,Server 可以提供实时的自动补全建议------就像 IDE 中的代码补全。
json
// 请求
{
"method": "completion/complete",
"params": {
"ref": { "type": "ref/prompt", "name": "code_review" },
"argument": { "name": "language", "value": "py" }
}
}
// 响应
{
"result": {
"completion": {
"values": ["python", "pytorch", "pyside"],
"total": 10,
"hasMore": true
}
}
}
补全结果最多返回 100 项,按相关性排序,并通过 hasMore 标记是否有更多结果。
18.4.2 上下文感知补全
当 Prompt 有多个参数时,已填写的参数值可以作为上下文传递,让补全更精准:
json
{
"method": "completion/complete",
"params": {
"ref": { "type": "ref/prompt", "name": "code_review" },
"argument": { "name": "framework", "value": "fla" },
"context": {
"arguments": { "language": "python" }
}
}
}
// 返回 ["flask"] 而非 ["flash", "flatbuffers", ...]
18.4.3 TypeScript SDK 中的 completable 封装
TypeScript SDK 提供了 completable 函数,将补全逻辑直接绑定到 Schema 定义上:
typescript
import { completable, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
server.registerPrompt(
'review-code',
{
title: 'Code Review',
argsSchema: z.object({
language: completable(
z.string().describe('Programming language'),
(value) => ['typescript', 'javascript', 'python', 'rust', 'go']
.filter(lang => lang.startsWith(value))
)
})
},
({ language }) => ({
messages: [{
role: 'user',
content: { type: 'text', text: `Review this ${language} code.` }
}]
})
);
completable 通过 Symbol 将补全回调函数附加到 Zod Schema 上,MCP Server 在收到 completion/complete 请求时自动调用对应的回调。这是一种优雅的元编程技巧------Schema 既是类型定义,又携带了行为。
18.5 Progress:进度追踪
18.5.1 长操作的用户体验
当工具执行耗时较长(如代码分析、大文件处理),用户需要知道"系统还在工作"。MCP 的进度追踪通过 progressToken 机制实现:
json
// Client 在请求中附带 progressToken
{
"method": "tools/call",
"params": {
"name": "analyze_codebase",
"arguments": { "path": "/src" },
"_meta": { "progressToken": "abc123" }
}
}
// Server 发送进度通知
{
"method": "notifications/progress",
"params": {
"progressToken": "abc123",
"progress": 50,
"total": 100,
"message": "正在分析第 50/100 个文件..."
}
}
设计要点:
progressToken可以是字符串或整数,由请求方选择,必须在所有活跃请求中唯一progress值必须单调递增,即使total未知progress和total可以是浮点数message提供人类可读的进度信息- 进度通知在请求完成后必须停止
18.6 Cancellation:优雅取消
用户可能在工具执行中途改变主意。MCP 通过 notifications/cancelled 支持请求取消:
json
{
"method": "notifications/cancelled",
"params": {
"requestId": "123",
"reason": "用户请求取消"
}
}
取消是"发射后遗忘"的------发送方不能假设取消一定成功。由于网络延迟,取消通知可能在请求完成后才到达,双方都必须优雅地处理这种竞态条件:
取消不能用于 initialize 请求。收到取消通知的一方应当停止处理、释放资源、不再发送对应的响应。
18.7 Logging:结构化日志
18.7.1 Server 到 Client 的日志流
Server 可以通过 notifications/message 向 Client 发送结构化日志,遵循 RFC 5424 的 syslog 级别:
| 级别 | 含义 | 示例 |
|---|---|---|
| debug | 详细调试信息 | 函数入口/出口 |
| info | 常规信息 | 操作进度 |
| notice | 正常但值得注意的事件 | 配置变更 |
| warning | 警告 | 使用了已废弃特性 |
| error | 错误 | 操作失败 |
| critical | 严重错误 | 组件故障 |
| alert | 需要立即处理 | 数据损坏 |
| emergency | 系统不可用 | 完全崩溃 |
Client 可以通过 logging/setLevel 动态调整日志级别,Server 只发送不低于设定级别的日志。
json
// Server 发送日志
{
"method": "notifications/message",
"params": {
"level": "error",
"logger": "database",
"data": {
"error": "Connection failed",
"details": { "host": "localhost", "port": 5432 }
}
}
}
日志的 data 字段是任意 JSON,这让 Server 可以传递丰富的结构化上下文,而不仅仅是文本消息。
18.7.2 安全约束
日志消息绝不能包含凭证、个人身份信息(PII)、或可能帮助攻击者的内部系统细节。实现应当做速率限制和内容审查。
18.8 特性间的协同
这些特性不是孤立的,它们在实际场景中深度协同。考虑一个"部署到生产环境"的工具调用:
"选择部署分支和环境" C->>U: 显示表单 U->>C: { branch: "main", env: "staging" } C->>S: accept Note over S: 发现没有 GitHub token S->>C: elicitation/create (url)
"请授权 GitHub 访问" C->>U: 显示 URL 确认 U->>C: 同意 Note over S: 等待 OAuth 完成... S-->>C: notifications/elicitation/complete Note over S: 开始部署 S-->>C: notifications/progress (10/100, "拉取代码...") S-->>C: notifications/message (info, "构建开始") S-->>C: notifications/progress (50/100, "运行测试...") S-->>C: notifications/progress (90/100, "推送镜像...") S-->>C: notifications/message (info, "部署完成") S->>C: 返回部署结果
在这个流程中,Roots 提供了上下文,Form Elicitation 收集了用户决策,URL Elicitation 完成了安全授权,Progress 追踪了长操作,Logging 提供了实时反馈。每个特性各司其职,共同构成了一个完整的交互体验。
18.9 本章小结
本章覆盖的六个特性看似零散,实则构成了 MCP 协议中最贴近用户体验的一层。如果说 Tools/Resources/Prompts 定义了 Server 能做什么,这些特性就定义了 Server 如何与人协作:
- Elicitation 让 Server 能够在需要时向用户提问,Form Mode 用于常规数据,URL Mode 用于敏感信息
- Roots 让 Server 感知 Client 的文件系统边界,做出更精准的决策
- Completion 通过自动补全提升用户输入参数的体验
- Progress 让长操作的进展透明可见
- Cancellation 赋予用户对运行中操作的控制权
- Logging 提供结构化的实时反馈通道
所有这些特性都通过能力协商(Capabilities)控制可用性,通过请求关联约束(Request Association)确保语义合理性。它们的存在让 MCP 从一个简单的"工具调用协议"进化为一个支持复杂人机协作流程的完整框架。
在下一章,我们将看到这些特性在一个真实的 12 万行代码库中如何被实际运用------Claude Code 的 MCP 客户端实现。