MCP协议设计与实现-第18章 Elicitation、Roots 与配置管理

《MCP 协议设计与实现》完整目录

第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 的配置管理与操作生命周期层:

graph TB subgraph "反向通道 --- Server → Client" E["Elicitation
向用户提问"] 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 :支持 minLengthmaxLengthpatternformat(email/uri/date/date-time)
  • number / integer :支持 minimummaximum
  • 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 的数据通道:

sequenceDiagram participant U as 用户 participant B as 浏览器 participant C as MCP Client participant S as MCP Server participant T as 第三方服务 C->>S: tools/call "连接 GitHub" Note over S: 需要 GitHub OAuth S->>C: elicitation/create (mode: url) C->>U: "Server 请求打开 URL,是否同意?" U->>C: 同意 C->>B: 打开 https://mcp.example.com/connect?id=xxx C->>S: { action: "accept" } B->>S: 加载连接页面 Note over S: 验证用户身份 S->>B: 重定向到 GitHub OAuth B->>T: GitHub 授权页面 U->>T: 授权 T->>S: 回调,返回 authorization code S->>T: 用 code 换取 token Note over S: 安全存储 token,绑定用户 S-->>C: notifications/elicitation/complete C->>S: 重试 tools/call S->>T: 使用存储的 token 调用 GitHub API S->>C: 返回工具结果

URL Mode 的安全规范极其严格:

  1. Server 不得在 URL 中包含用户敏感信息
  2. Server 不得提供预认证的 URL
  3. Client 必须显示完整 URL 并获得用户明确同意后才能打开
  4. Client 必须使用安全的浏览器环境(如 iOS 的 SFSafariViewController),而非可被宿主应用窥探的 WebView
  5. 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/callresources/readprompts/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 未知
  • progresstotal 可以是浮点数
  • message 提供人类可读的进度信息
  • 进度通知在请求完成后必须停止

18.6 Cancellation:优雅取消

用户可能在工具执行中途改变主意。MCP 通过 notifications/cancelled 支持请求取消:

json 复制代码
{
  "method": "notifications/cancelled",
  "params": {
    "requestId": "123",
    "reason": "用户请求取消"
  }
}

取消是"发射后遗忘"的------发送方不能假设取消一定成功。由于网络延迟,取消通知可能在请求完成后才到达,双方都必须优雅地处理这种竞态条件:

sequenceDiagram participant C as Client participant S as Server C->>S: tools/call (ID: 42, progressToken: "t1") Note over S: 开始处理 S-->>C: progress (25/100) S-->>C: progress (50/100) C--)S: notifications/cancelled (ID: 42) alt 取消成功 Note over S: 停止处理,释放资源 else 已经完成 S->>C: 返回结果(Client 应忽略) end

取消不能用于 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 特性间的协同

这些特性不是孤立的,它们在实际场景中深度协同。考虑一个"部署到生产环境"的工具调用:

sequenceDiagram participant U as 用户 participant C as Client participant S as MCP Server participant G as GitHub API C->>S: tools/call "deploy_to_production" Note over S: 检查是否有 GitHub 凭证 S->>C: roots/list(查看工作目录) C->>S: 返回 ["/home/user/myapp"] S->>C: elicitation/create (form)
"选择部署分支和环境" 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 客户端实现。

相关推荐
杨艺韬2 小时前
MCP协议设计与实现-第17章 sampling
agent
杨艺韬2 小时前
MCP协议设计与实现-第16章 服务发现与客户端注册
agent
杨艺韬3 小时前
MCP协议设计与实现-第8章 TypeScript Server 实现剖析
agent
杨艺韬3 小时前
MCP协议设计与实现-第04章 生命周期与能力协商
agent
杨艺韬3 小时前
MCP协议设计与实现-第14章 SSE 与 WebSocket
agent
杨艺韬3 小时前
MCP协议设计与实现-第6章 Resource:结构化的上下文注入
agent
杨艺韬3 小时前
MCP协议设计与实现-第10章 Python Server 实现剖析
agent
杨艺韬3 小时前
MCP协议设计与实现-第12章 STDIO 传输:本地进程通信
agent
杨艺韬3 小时前
MCP协议设计与实现-第02章 架构总览:Host-Client-Server 模型
agent