MCP高级特性与最佳实践
在掌握 MCP 基础(MCP开发与应用)之后,本文聚焦协议规范中真实存在的高级能力——资源订阅、Roots、Sampling、Elicitation——以及可观测性与工程化最佳实践。所有代码均基于唯一官方 SDK
@modelcontextprotocol/sdk,可直接作为面试回答。
目录
- ⚠️ 先澄清:哪些是真能力,哪些是虚构
- 🔔 资源订阅与变更通知
- 📁 Roots:宿主向 Server 声明可访问根
- 🤖 Sampling:Server 反向请求 LLM
- 💬 Elicitation:Server 向用户补充信息
- 📊 可观测性:用 OpenTelemetry 包装 tools/call
- 🔐 安全与权限最佳实践
- 🧩 工具链编排归 Agent 层,MCP 保持原子工具
- 📝 总结
⚠️ 先澄清:哪些是真能力,哪些是虚构
很多旧教程里出现过下面这些「方法」,它们并不在 MCP 规范或官方 SDK 中,属于虚构,请勿使用:
| 虚构 API(❌ 不存在) | 应使用的真实能力(✅) |
|---|---|
enableDynamicDiscovery / discoveryInterval | 规范的 listChanged 能力 + notifications/tools/list_changed 等列表变更通知 |
batchReadResources / batchCallTools | 没有批量原语;并发多次 resources/read / tools/call,编排放在 Agent 层 |
setAuthorizationPolicy | 远程用 OAuth 2.1;权限在 Server 实现内/宿主侧落地 |
registerToolChain / callToolChain | MCP 保持原子工具,链路编排交给 LangGraph / OpenAI Agents SDK 等 |
下面介绍的都是 spec 中真实存在、且常被面试问到的高级能力。
🔔 资源订阅与变更通知
MCP 资源支持订阅 + 主动通知,让宿主无需轮询即可感知变化。能力协商时 Server 在 capabilities.resources 中声明 subscribe: true / listChanged: true。
- 内容更新:Client 发
resources/subscribe(带uri),Server 在内容变化时推送notifications/resources/updated。 - 列表变化:Server 推送
notifications/resources/list_changed,提示客户端重新resources/list。
// 服务端:资源变化时主动通知订阅者(官方 SDK)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer(
{ name: "docs-server", version: "1.0.0" },
{ capabilities: { resources: { subscribe: true, listChanged: true } } }
);
// 当底层文档发生变化时,通知客户端某个资源已更新
async function onDocChanged(uri) {
await server.server.sendResourceUpdated({ uri });
}
// 当资源列表本身发生增减时
async function onResourceListChanged() {
await server.server.sendResourceListChanged();
}
// 客户端:订阅并监听更新通知
import { ResourceUpdatedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
await client.subscribeResource({ uri: "file:///app/config.json" });
client.setNotificationHandler(ResourceUpdatedNotificationSchema, async (n) => {
const fresh = await client.readResource({ uri: n.params.uri });
console.log("资源已更新,重新读取:", fresh);
});
📁 Roots:宿主向 Server 声明可访问根
Roots 是 Client(宿主)能力:宿主向 Server 声明一组「根」URI(通常是文件系统目录或项目工作区),告诉 Server「你应当在这些边界内工作」。这是 MCP 的安全/作用域边界机制——比如把文件型 Server 限制在当前工作区目录内。
- Client 在
initialize时声明capabilities.roots(可含listChanged)。 - Server 可调用
roots/list获取根列表;根变化时 Client 推送notifications/roots/list_changed。
// 客户端:声明支持 roots,并提供当前工作区根
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
const client = new Client(
{ name: "my-ide", version: "1.0.0" },
{ capabilities: { roots: { listChanged: true } } }
);
client.setRequestHandler(/* ListRootsRequestSchema */ ListRootsSchema, async () => ({
roots: [{ uri: "file:///Users/me/project", name: "当前项目" }],
}));
🤖 Sampling:Server 反向请求 LLM
Sampling 让 Server 反向向宿主请求一次 LLM 补全(sampling/createMessage)。这样 Server 内部需要「智能」时,无需自己持有模型 API Key,而是借用宿主的模型——并且宿主可对每次采样做人工审批与限额,是重要的安全/成本控制点。
// 服务端工具内部:请求宿主的 LLM 完成一次推理
server.tool("summarize", "对长文本做摘要", { text: z.string() }, async ({ text }) => {
const completion = await server.server.createMessage({
messages: [
{ role: "user", content: { type: "text", text: `用三句话总结:\n${text}` } },
],
maxTokens: 200,
});
return { content: [{ type: "text", text: completion.content.text }] };
});
Server 不应假定 sampling 一定可用:宿主可能未实现,或要求用户逐次批准。面试要点:Sampling 是「Server→Host→LLM」的反向链路,主权与审批在宿主。
💬 Elicitation:Server 向用户补充信息
Elicitation(较新版本规范引入)允许 Server 在执行过程中向用户索取结构化输入(elicitation/create),例如缺少必填参数、需要二次确认时,由宿主弹出表单收集,再继续执行。它配合 JSON Schema 描述需要的字段。
// 服务端:执行中向用户索取缺失信息
const result = await server.server.elicitInput({
message: "请提供要部署的目标环境",
requestedSchema: {
type: "object",
properties: { env: { type: "string", enum: ["staging", "production"] } },
required: ["env"],
},
});
// result.action: "accept" | "decline" | "cancel"
📊 可观测性:用 OpenTelemetry 包装 tools/call
MCP 规范本身不提供 metrics 配置项(旧教程里的 metrics: { endpoint } 是虚构)。生产中的做法是在 Server 侧用 OpenTelemetry 给每次 tools/call 打 span,记录工具名、入参摘要、耗时、成败、错误码,并接入 Jaeger / Tempo / Datadog 等后端。
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("mcp-server");
function instrumentedTool(name, description, schema, handler) {
server.tool(name, description, schema, async (args, extra) => {
return tracer.startActiveSpan(`tool.${name}`, async (span) => {
span.setAttribute("mcp.tool.name", name);
try {
const res = await handler(args, extra);
span.setStatus({ code: SpanStatusCode.OK });
return res;
} catch (err) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
throw err;
} finally {
span.end();
}
});
});
}
配合宿主侧的「任务级 trace id」,即可端到端回放「模型决策 → 工具调用 → 数据源访问」的全链路(详见 AI应用架构设计 的可观测性章节)。
🔐 安全与权限最佳实践
- 细粒度权限在 Server 内落地:用真实的鉴权中间件/数据库角色控制每个工具能访问的数据,而不是依赖虚构的
setAuthorizationPolicy。 - 远程鉴权用 OAuth 2.1:Streamable HTTP Server 作为 OAuth 资源服务器,校验访问令牌、限定 scope、使用 PKCE。
- 传输安全:远程一律 HTTPS(
wss/https),校验Origin、绑定会话 ID,防止 DNS 重绑定攻击。 - 高危工具 HITL:删除/转账/写生产等操作交由宿主人工确认;Sampling 与 Elicitation 也应受用户审批。
- 供应链审计:第三方 Server = 第三方代码,安装前审查来源与依赖、锁定版本,纳入发布清单。
🧩 工具链编排归 Agent 层,MCP 保持原子工具
一个常见误区是想让 MCP「内置工具链/流水线」。正确分层是:
- MCP Server:只暴露原子、单一职责的工具(
fetch_data、run_query、send_email…),无状态、可组合。 - Agent 层:由 LangGraph(状态图)、OpenAI Agents SDK(handoff/guardrails)、Vercel AI SDK 等负责把多个工具编排成多步工作流、做条件分支、重试与补偿。
这样 MCP Server 可被任意宿主复用,编排逻辑也不被锁死在协议层,符合「关注点分离」。
📝 总结
MCP 的高级能力是资源订阅、Roots、Sampling、Elicitation,外加用 OpenTelemetry 自建可观测性;权限靠 Server 实现与 OAuth 2.1,编排交给 Agent 层。务必避开 enableDynamicDiscovery、batchCallTools、setAuthorizationPolicy、registerToolChain 这类虚构 API——它们既不在规范里,也不在官方 SDK 中。坚持「Server 提供原子能力、宿主掌握主权与审批、Agent 负责编排」的分层,是构建安全可扩展 MCP 系统的关键。