MCP Server 设计:产品团队也要理解工具边界
从一个翻车现场说起
我在上一个季度参与了一个内部客服系统的 Agent 化改造。产品经理的诉求很直接:让 AI 助手能自动查订单、发退款、改地址。技术方案选了 MCP(Model Context Protocol)作为 Agent 和后端服务之间的桥梁。上线第一周,就出了两件事:一是 Agent 把「查询订单」和「修改订单地址」混在一个工具调用里完成,导致地址被静默改掉,客服和顾客都没收到通知;二是退款工具没有要求用户确认,Agent 根据对话上下文自动创建了一笔 2000 元的退款申请。
这两个问题的根源不在 Agent 的推理能力,而在于工具边界没有被正确设计。MCP Server 暴露了什么工具、工具的粒度怎么划分、哪些操作需要确认——这些决策在产品需求评审阶段就被忽略了,等到上线才发现 Agent 的行为远超预期。
这篇文章把我在 MCP Server 设计上踩过的坑和总结出来的方法论写下来。面向的不只是写代码的工程师,也包括需要理解工具边界的产品经理和技术负责人。
MCP 的核心概念与设计约束
MCP 是 Anthropic 在 2024 年 11 月发布的开放协议,目标是让 AI 应用以一种标准化的方式连接外部数据源和工具1。2025 年 3 月 OpenAI 宣布支持后,MCP 迅速成为 Agent 集成领域的事实标准。到 2026 年,Python 和 TypeScript SDK 的月下载量已经达到约 9700 万次,Registry 里有近 2000 个 Server 条目2。
MCP 的架构遵循 Client-Server 模式,有三个关键角色:
- Host:用户直接交互的 AI 应用,比如 Claude Desktop、VS Code、Cursor
- Client:Host 内部用来管理与 Server 连接的组件,一个 Host 可以同时连接多个 Server
- Server:通过协议向 Client 暴露能力的服务端程序
Server 可以暴露三种核心原语(Primitives):
| 原语 | 定义 | 典型场景 | 谁在用 |
|---|---|---|---|
| Tools | Agent 可调用的可执行函数 | 创建订单、发送通知、执行查询 | AI 模型通过 Agent 调用 |
| Resources | 只读的数据源 | 数据库记录、文件内容、API 响应 | AI 模型读取上下文 |
| Prompts | 预定义的交互模板和工作流 | 标准客服回复模板、代码审查流程 | 用户或产品 UI 选择调用 |
这三种原语的设计决策直接决定了 Agent 的行为边界。Tools 决定了 Agent 能「做什么」,Resources 决定了 Agent 能「看到什么」,Prompts 决定了 Agent 按什么「套路」做事。
底层传输层有两种选择:本地使用 Stdio 传输(通过标准输入输出通信,零网络开销),远程使用 Streamable HTTP 传输(支持 SSE 流式响应和标准 HTTP 认证)3。
MCP 协议基于 JSON-RPC 2.0,是有状态的协议。连接建立时需要经过能力协商(Capability Negotiation),Client 和 Server 各自声明支持的原语类型和特性。这个设计意味着 Server 不能假设 Client 知道它有什么工具——必须通过 tools/list 方法主动暴露。
鉴权流程:不能跳过的安全边界
MCP 的授权规范基于 OAuth 2.1,将 MCP Server 定位为 OAuth 资源服务器(Resource Server),MCP Client 作为 OAuth 客户端4。整个鉴权流程涉及四个参与方:
这个流程中有几个产品设计上容易忽略的关键点:
第一,Token 必须包含在每个 HTTP 请求的 Authorization 头中,不能放在 URL 查询参数里。这意味着 MCP Server 的每一个接口都要验证 Token,没有「免认证」的便捷通道。
第二,Scope 管理是分层的。Server 在返回 403 时可以通过 WWW-Authenticate 头的 scope 字段告知 Client 还需要哪些额外权限,Client 会发起 Step-Up 授权流程请求新的 Token。这个机制对产品团队意味着:不同工具需要声明不同的权限级别,而不是一刀切地给一个 admin scope。
第三,Resource Indicators(RFC 8707)要求 Token 绑定到特定的 MCP Server。Token 不能跨 Server 使用,这防止了一个 Server 拿着别人的 Token 去调用另一个 Server。
工具粒度设计:三个真实案例
工具粒度是 MCP Server 设计中最影响 Agent 行为的决策。Anthropic 的工程博客明确指出:「更多工具不一定会带来更好的结果。重叠或过于细碎的工具会迷惑 Agent,或者让它偏离高效策略」5。
案例一:订单查询 vs 订单修改混在一起
场景:客服系统需要一个工具让 Agent 帮用户查订单和处理地址变更。
翻车:我最初设计了一个 manage_order 工具,接受一个 action 参数(query / update_address / cancel)。Agent 在对话中同时完成查询和修改,地址变更没有触发通知流程。
修复:拆成两个独立工具,查询工具标记为只读,修改工具要求用户确认并写入审计日志。
// ❌ 错误做法:一个工具覆盖多个动作,权限边界模糊
interface ManageOrderInput {
orderId: string
action: 'query' | 'update_address' | 'cancel' // Agent 可能在一个调用中混合操作
address?: string
}
// ✅ 正确做法:按业务动作拆分,权限和控制分离
interface QueryOrderInput {
orderId: string
// 只读操作,不需要用户确认
}
interface UpdateOrderAddressInput {
orderId: string
newAddress: string
// 写入操作,Server 端强制要求用户确认
// 写入审计日志,触发通知
}核心差异在于:只读操作和写入操作被物理隔离。查询工具不需要确认,修改工具在 Server 端有强制的确认和审计逻辑。Agent 即使想跳过确认也做不到,因为确认是 Server 端的行为,不是 Agent 端的 prompt 约束。
案例二:日志查询工具的 Token 爆炸
场景:Agent 需要根据用户描述的问题搜索系统日志。
翻车:第一版 get_logs 工具返回最近 1000 行日志原文。Agent 的上下文窗口直接被塞满,后续推理质量严重下降,而且 Token 消耗暴涨。
修复:改成搜索式工具,支持关键词过滤、时间范围和返回格式控制。
// ❌ 错误做法:返回全量日志,不管 Agent 是否需要
interface GetLogsInput {
service: string
}
// 返回:最近 1000 行完整日志(约 50000 tokens)
// ✅ 正确做法:搜索式接口,支持过滤和格式控制
interface SearchLogsInput {
service: string
keyword: string // 必填,强制 Agent 精确搜索
timeRange?: '1h' | '6h' | '24h' | '7d'
limit?: number // 默认 20,最大 100
responseFormat?: 'concise' | 'detailed'
}
// concise 模式返回:
// "在 2026-06-25 14:23:01 发现 3 条 ERROR:
// 1. PaymentService: timeout connecting to gateway (retried 3 times)
// 2. PaymentService: order #9182 payment failed - insufficient funds
// 3. OrderService: order #9182 status changed to PAYMENT_FAILED"
// detailed 模式额外返回 trace_id、log_id 等,供下游工具使用Anthropic 的实践建议是:工具应该在内部处理中间数据,只把最终必要的结果暴露给 Agent5。Agent 不需要看到 1000 行日志原文,它需要的是一个结构化的摘要和可操作的线索。
案例三:跨服务操作缺少命名空间
场景:产品同时接入了 Slack 通知和邮件通知两个渠道。
翻车:两个服务分别暴露了 send_message 工具。Agent 在需要发 Slack 消息时调用了邮件渠道的 send_message,因为两个工具的描述太相似,Agent 无法区分。
修复:用命名空间前缀区分工具归属。
// ❌ 错误做法:无命名空间,工具名冲突
// Server A: send_message(to: string, content: string)
// Server B: send_message(to: string, content: string)
// Agent 看到两个同名工具,随机选一个
// ✅ 正确做法:按服务 + 资源分层命名
// Slack Server: slack_channels_list, slack_send_message
// Email Server: email_send_message, email_list_templates
// 工具名自解释,Agent 能根据前缀判断归属命名空间的粒度需要根据实际场景调整。Anthropic 建议可以按服务名(asana_search vs jira_search)或者按资源类型(asana_projects_search vs asana_users_search)来划分,具体用前缀还是后缀最好通过评估来决定5。
输入 Schema 设计:Agent 的认知负荷
工具的 inputSchema 不只是一份类型声明——它是 Agent 理解如何使用这个工具的唯一依据。MCP 规范要求每个工具提供 JSON Schema 格式的输入定义3,但 schema 写得好不好,直接决定 Agent 的调用成功率。
| 设计维度 | 差的做法 | 好的做法 | 原因 |
|---|---|---|---|
| 参数命名 | user、data、type | user_id、order_data、refund_type | Agent 靠参数名推断含义,模糊命名导致误用 |
| 描述文案 | 「执行操作」 | 「创建一个退款申请,需要订单 ID 和退款原因,金额不能超过原订单实付金额」 | 描述即 prompt,越具体 Agent 越不容易犯错 |
| 枚举约束 | type: string | type: enum ['full', 'partial'] | 枚举值限制 Agent 的自由发挥空间 |
| 必填/可选 | 全部必填或全部可选 | 核心参数必填,格式参数可选且有默认值 | 减少 Agent 需要猜测的参数数量 |
| 错误提示 | Error 400: Invalid input | 参数 date 格式错误,要求 YYYY-MM-DD,收到的是 2026/06/26 | 可操作的错误信息让 Agent 能自我纠正 |
// ❌ 错误做法:描述模糊,缺少约束
{
name: 'process',
description: '处理请求',
inputSchema: {
type: 'object',
properties: {
data: { type: 'string' },
type: { type: 'string' }
},
required: ['data', 'type']
}
}
// ✅ 正确做法:描述清晰,约束明确
{
name: 'create_refund_request',
title: '创建退款申请',
description: '为指定订单创建退款申请。退款金额不能超过原订单的实付金额。'
+ '退款原因必须是以下之一:商品质量问题、发货错误、用户主动取消、其他。'
+ '创建成功后会触发审计日志,并通过邮件通知用户。',
inputSchema: {
type: 'object',
properties: {
order_id: {
type: 'string',
description: '订单 ID,格式为 ORD-XXXXXXXX(8 位数字)'
},
refund_amount_cents: {
type: 'integer',
description: '退款金额(单位:分),必须大于 0 且不超过订单实付金额',
minimum: 1
},
reason: {
type: 'string',
enum: ['quality_issue', 'shipping_error', 'user_cancelled', 'other'],
description: '退款原因分类'
},
note: {
type: 'string',
description: '补充说明(可选),最多 500 字',
maxLength: 500
}
},
required: ['order_id', 'refund_amount_cents', 'reason']
}
}读写的权限分层与用户确认
MCP 协议本身不区分读操作和写操作——这个区分需要 Server 端自行实现。但对产品团队来说,这个区分至关重要,因为它直接决定了哪些操作需要用户确认、哪些可以自动执行。
| 操作类型 | 示例 | 是否需要用户确认 | 审计要求 |
|---|---|---|---|
| 只读查询 | 查订单、查库存、查日志 | 否 | 记录调用日志 |
| 内部写入 | 更新备注、添加标签 | 建议确认 | 完整审计 + 变更记录 |
| 外部影响 | 发邮件、调第三方 API | 是 | 完整审计 + 发送记录 |
| 资金操作 | 退款、改价、扣款 | 强制确认 | 完整审计 + 财务对账 |
| 删除操作 | 删除记录、取消订单 | 强制确认 | 完整审计 + 软删除 |
MCP 协议提供了 Elicitation 原语,允许 Server 在执行过程中暂停并向用户请求输入或确认3。这个机制是实现用户确认流程的技术基础。但产品团队需要定义清楚:哪些工具使用 Elicitation、确认界面展示什么信息、用户拒绝后怎么回退。
// ✅ 在 Server 端实现操作分级
enum OperationLevel {
READ = 'read', // 只读,自动执行
INTERNAL_WRITE = 'write', // 内部写入,建议确认
EXTERNAL_IMPACT = 'external', // 外部影响,需要确认
FINANCIAL = 'financial', // 资金操作,强制确认
DESTRUCTIVE = 'destructive' // 删除操作,强制确认 + 软删除
}
// 工具注册时声明操作级别
server.tool(
'create_refund_request',
{ /* inputSchema */ },
async (args, context) => {
// Server 端强制:金融操作必须经过用户确认
if (context.operationLevel === 'financial' && !context.userConfirmed) {
return {
content: [{
type: 'text',
text: '需要用户确认:即将为订单 ORD-12345678 创建 ¥20.00 的退款申请,'
+ '退款原因:商品质量问题。请确认后继续。'
}],
// 使用 MCP Elicitation 机制请求用户确认
_meta: { requiresConfirmation: true }
}
}
// 执行退款逻辑...
}
)另一个容易忽略的点是:MCP Server 不应该绕过已有的权限系统。如果用户 A 在产品 UI 中没有退款权限,那通过 MCP 调用的 Agent 也不应该有。权限校验应该在 Server 端统一执行,而不是依赖 Agent 的 prompt 约束。
好工具与坏工具的系统对比
经过几个项目的实践,我把 MCP 工具设计的关键维度整理成对比表:
| 维度 | 坏工具的特征 | 好工具的特征 |
|---|---|---|
| 粒度 | 一个工具做所有事,或每个 API 端点对应一个工具 | 围绕稳定的业务动作设计,一个工具完成一个完整意图 |
| 命名 | process、handle、do_action | create_refund_request、search_order_by_tracking |
| 输入 | 参数少、描述短、无约束 | 参数有类型约束、有枚举值、描述即使用说明 |
| 输出 | 返回完整 JSON、技术 ID、全部字段 | 返回 Agent 可理解的自然语言摘要,可选详细模式 |
| 错误 | 返回 HTTP 状态码或异常堆栈 | 返回可操作的错误描述,告诉 Agent 怎么修 |
| 权限 | 所有工具同一个权限级别 | 按操作类型分级,写入和删除需要确认 |
| 幂等性 | 重复调用产生重复数据 | 支持幂等键或检查重复,安全重试 |
| Token 效率 | 无分页、无过滤、返回全量数据 | 支持分页、过滤、简洁/详细两种响应模式 |
产品指标设计
上线只是开始。工具是否真的好用,需要持续的数据来验证。
| 指标 | 定义 | 健康阈值 | 异常信号 |
|---|---|---|---|
| 工具调用成功率 | 成功调用 / 总调用次数 | > 95% | 低于 90% 说明工具设计或后端有问题 |
| 确认取消率 | 用户取消确认 / 总确认请求 | < 30% | 过高说明 Agent 在不该发起确认时发起了 |
| 人工接管率 | 需要人工介入 / 总任务数 | < 20% | 过高说明工具能力覆盖不够 |
| 平均调用步数 | 完成一个业务任务的平均工具调用次数 | 因场景而异 | 突然增加说明工具粒度可能变了 |
| Token 消耗 | 每次任务平均消耗的 Token 数 | 持续优化 | 异常升高说明工具返回了过多无效信息 |
| 工具选择准确率 | Agent 选择了正确工具 / 总调用次数 | > 98% | 低于 95% 说明工具命名或描述有问题 |
这些指标需要在 MCP Server 的审计日志基础上建立。每次工具调用都应该记录:调用方、工具名、输入参数、输出结果、耗时、是否成功、是否经过用户确认。
上线前检查清单
设计阶段
- 每个工具是否围绕单一业务动作设计?不存在一个工具处理多种动作的情况
- 工具命名是否使用
服务名_动作_对象的格式?是否存在跨 Server 的工具名冲突? - 输入 Schema 的每个参数是否都有清晰的描述和类型约束?枚举参数是否限制了可选值?
- 工具描述是否足够详细,相当于写给一个不了解系统的新人看的使用说明?
- 输出格式是否对 Agent 友好?是否区分了简洁模式和详细模式?
安全阶段
- 只读操作和写入操作是否拆分为独立工具?
- 写入、删除、资金操作是否实现了用户确认流程?
- MCP Server 是否复用了产品已有的权限系统,而不是独立实现一套?
- 是否实现了完整的审计日志,覆盖每次工具调用的输入、输出和操作人?
- 远程 Server 是否实现了 OAuth 2.1 鉴权?Token 是否绑定到了特定的 Server?
运维阶段
- 是否实现了工具调用的监控指标(成功率、耗时、Token 消耗)?
- 错误响应是否包含可操作的修复建议,而不只是状态码?
- 是否支持工具列表变更通知(
tools/list_changed),让 Client 能感知工具更新? - 是否准备了工具降级方案?当后端服务不可用时,工具是否返回有意义的错误而不是超时?
小结
MCP Server 的设计决策会直接影响 Agent 的行为边界。工具粒度决定了 Agent 的行动自由度,输入 Schema 决定了 Agent 的操作准确度,权限分层决定了 Agent 的安全边界。这些不是纯技术问题——产品经理需要参与工具边界的设计,因为工具暴露了什么能力,就意味着 Agent 可能做什么事。
不要等到上线翻车后才补设计。
参考资料
Footnotes
-
Anthropic. Introducing the Model Context Protocol. 2024-11. ↩
-
WorkOS. Everything Your Team Needs to Know About MCP in 2026. 2026-03. ↩
-
Model Context Protocol. Architecture Overview. 2025-06-18 Specification. ↩ ↩2 ↩3
-
Model Context Protocol. Authorization Specification. Draft Specification. ↩
-
Anthropic Engineering. Writing Effective Tools for AI Agents. 2025-09. ↩ ↩2 ↩3