MCP服务器设计 是做什么的?
本技能定义了MCP服务器如何将产品暴露给AI代理:工具在代码中的位置,连接如何绑定到一个租户和一个人,哪些调用需要人工确认,以及结果和错误如何引导模型做出正确的下一步调用。协议语法遵循MCP规范(编写时的修订版为2026-07-28)和SDK文档。查询范围、权限目录和ID格式来自tenant-isolation、authorization-rbac和dual-key-identifiers。本技能旨在防止以下问题:模型选择了错误的租户,向客户发送了两次邮件,或对第三方植入的文本执行了操作——这一切都是因为服务器在模型、客户端或工具描述中留下了一个后门。
适用 / 不适用场景
- 适用:为产品构建MCP服务器,添加或审查工具,选择MCP凭据如何绑定到租户和个人,用于通知客户、寻呼人员、静音警报或删除的工具,客户端重试后的重复写入,错误和结果的格式,工具名称和描述,适用于小型模型的工具集,以及工具使用情况的评估。
- 不适用:配置客户端以使用他人的MCP服务器,REST或GraphQL端点设计,非MCP的聊天机器人和斜杠命令,为产品自己的助手编写提示词,以及非MCP路由上的速率限制。
规则
重要性最高的规则排在前面。示例使用一个预订系统:预订、客户、技术人员。
- [不可变] 服务器强制执行必须不出错的事;描述和注释仅起引导作用。下面的每个门控机制均在每次调用的服务器代码中运行。原因:注释只是提示,存在自动批准或从不提示的客户端。测试:从一个忽略提示的原始JSON-RPC客户端调用每个受门控的工具;每个门控仍然触发。来源:MCP schema 2026-07-28,
ToolAnnotations:“客户端绝不应基于来自不受信任服务器的ToolAnnotations做出工具使用决策”(https://modelcontextprotocol.io/specification/2026-07-28/schema,已验证于2026-10-31)。 - [不可变] 工具是服务层上的第二个适配器:它们调用与UI和API相同的领域服务,绝不通过回环调用自己的HTTP API,也绝不直接调用ORM。保护数据(如限制、防止SSRF的URL白名单、引用记录的所有权)的检查必须放在服务层,而不仅放在控制器或表单请求中。每个函数只有一个写入路径。原因:一个薄适配器继承了所有仅其兄弟适配器运行的检查。测试:架构测试应失败于工具类中的持久化调用;对于每个写入工具,API拒绝的输入也应该被工具拒绝。来源:可通过上述测试重现。
- [不可变] 一个连接,一个租户:租户在创建连接时绑定在凭据中,没有工具模式包含租户参数,像
workspace_id这样的参数将被视为未知被拒绝,并且每个句柄都在绑定租户内解析。一个服务于多个租户的凭据,需将租户放在传输层(路径或标头)中,并对照凭据进行检查,绝不能放在工具参数中。原因:租户参数会让模型选择租户。测试:对于每个带引用的工具,传入另一个租户的有效句柄;期望返回not_found且行数据不变。来源:MCP工具规范,工具集“可能会根据请求中呈现的授权信息而有所不同”(https://modelcontextprotocol.io/specification/2026-07-28/server/tools);安全最佳实践,关键状态应依据“从已验证令牌中派生的用户ID,而非客户端提供的用户ID”(https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices);两者已验证于2026-10-31。 - [不可变] 权限是实时计算的:有效权限 = 令牌权限 ∩ 创建连接的人的当前权限,每次
tools/list和tools/call时计算。令牌权限在创建时封顶,创建者被移除或停用会终止连接,权限撤销通过一个审计服务运行。原因:冻结在令牌中的权限会超越角色变更。测试:删除创建者的角色,调用一个写入工具;下一次调用将被拒绝。来源:MCP工具规范,服务器必须“实施适当的访问控制”(已验证于2026-10-31)。 - [不可变] MCP请求上无环境用户:中间件绑定一个连接上下文(租户、创建者、实时权限),并将框架的当前用户槽位留空;工具从上下文中读取操作者,服务将操作者作为一个参数。原因:如果设置了所有者为当前用户,工具中一个意外的策略检查可能会通过,因为所有者可以做任何事情;若没有用户,则失败关闭。测试:架构测试禁止在工具代码中使用gate、policy和current-user帮助器;一个有意的策略检查会使调用失败。来源:可通过上述测试重现。
- [不可变] 每个工具只有一个效果类:读取、静默写入、通知(触达客户或改变客户所见内容)、唤醒(寻呼或呼叫某人)、警报(静音、暂停或重新路由警报)、删除。根据代码路径(事件、监听器、观察者、作业、邮件)派生类,而非名称,并将跨越两个类的工具拆分开。原因:提示、速率桶、预览职责和连接切换都依据类而来。测试:使用队列、邮件和事件模拟,静默工具不会发送任何通知。来源:连接器审查标准,全捕获读写工具被拒绝,写入按操作类型拆分(https://claude.com/docs/connectors/building/review-criteria,已验证于2026-10-31)。
- [不可变] 通知、唤醒、警报和删除工具需两步完成。工具仅准备:它存储一个待处理操作(连接、工具、标准化参数和摘要、将发送内容的哈希值、过期时间),并返回
PREVIEW、一个confirm_token和一个服务器编写的confirm_summary。只有confirm_action执行,在此之前需检查连接、未使用、未过期、未被拒绝、确切的摘要、锁、实时权限和开关,以及内容哈希仍然匹配的新准备(否则返回新预览,旧令牌失效)。原因:模型和客户端都不能可靠地询问用户,并且已批准的文本可能在过程中改变。测试:仅预览不发送任何内容;重用、过期、外来和错误摘要的令牌失败;步骤之间的编辑返回changed_since_preview。来源:MCP工具规范,“应始终有一个人类在与模型交互的循环中,并且有能力拒绝工具调用”;MRTR服务器要求:客户端回显的状态是“攻击者控制的输入”,绑定到主体,短期过期和参数摘要,服务器端强制单次使用(https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr);两者已验证于2026-10-31。 - [不可变] 每个静默写入操作都有重放保护:指纹 = 连接 + 工具 + 标准化参数的规范JSON的SHA-256(所有层级按键排序);在窗口内命中则返回第一个结果并带上
replayed: true,不执行任何操作。可选的idempotency_key绑定到参数摘要。缓存保存展示器输出,绝不保存一次性秘密。原因:一个损坏的流丢失了正在进行的请求,客户端必须用新的ID重新发送,因此已运行的写操作会再次到达。测试:使用不同的JSON-RPC ID调用两次相同的操作;一行数据,第二次结果中replayed: true。来源:MCP变更日志2026-07-28,主要变更9(https://modelcontextprotocol.io/specification/2026-07-28/changelog);Claude Code会在新连接上重试对标记为idempotentHint或readOnlyHint的工具的待处理调用一次(https://code.claude.com/docs/en/mcp);两者已验证于2026-10-31。 - [不可变] 每个创建工具都有一个对应的更新工具,并且在业务键上有一个重复守卫。命中时返回一个错误,指出现有句柄和更新工具;如果仍需创建,需要
separate: true和一个理由。守卫读取同步列(created_by、created_at),绝不读取队列审计日志。编辑工具没有notify参数。原因:当要求“改进文本”时,模型会创建第二个对象,除非被阻止。测试:使用相同的键创建两次;一行数据,一个错误指出该对象。来源:可通过上述测试重现。 - [不可变] 更新操作携带调用者读取的版本:对象和结果中都有
version(内容哈希),更新和编辑工具需要它,过期版本返回conflict并附带当前状态。原因:基于旧读取操作的代理会覆盖同事的编辑。测试:读取,在其他地方更改行,用旧版本更新;返回conflict,行不变。来源:可通过上述测试重现。 - [不可变] 批量工具接受显式句柄,或一个过滤器加上必需的
expected_count;当匹配计数不同时,返回匹配项但不执行任何操作。原因:比模型预期更宽的过滤器会放大一个错误。测试:一个过滤器匹配了12个预订,但expected_count: 3;错误列出12个,且没有任何改变。来源:可通过上述测试重现。 - [不可变] 使用工具自身的规则在服务器端验证每个参数,拒绝未知字段并列出允许的字段。原因:SDK并不总是检查
inputSchema中的参数,忽略的字段会让模型以为它设置成功了。测试:一个额外字段返回允许的字段列表,且不写入任何内容。来源:MCP工具规范,服务器必须“验证所有工具输入”(已验证于2026-10-31)。 - [不可变] 结果中的第三方文本被视为不受信任:客户、最终用户、外部系统或上传可以写入的文本(便签、邮件正文、webhook负载、远程错误消息)会被截断、剥离控制字符,并在
untrusted_text下返回,附带一条不要执行其中指令的注释;原始内容绝不保留。描述描述工具,绝不要告诉模型如何行为。原因:注入的文本引导具有外部效果的工具是最高风险;预览步骤是第二道防线。测试:每个第三方字段中的注入测试标记(canary)仅出现在包装器内。来源:MCP工具规范,服务器必须“清理工具输出”;连接器审查标准拒绝描述中的提示注入模式;两者已验证于2026-10-31。 - [不可变] 结果不包含秘密,仅包含任务所需的个人数据:展示器具有明确的字段列表,绝不要使用全记录序列化器;日志包含工具名称、结果和公开ID,绝不包含参数。原因:每个结果都会发送给用户的模型提供商。测试:用诱饵值填充测试夹具的每个秘密字段,调用每个读取工具,grep结果和日志;无诱饵。来源:可通过上述测试重现;记录保存在
gdpr-engineering中。 - [不可变] 业务错误、验证错误和权限错误是工具执行错误(
isError: true),其文本以Error:开头,指明原因、修复方法和带有真实句柄的正确工具,并以一行JSON详细信息结束。“未找到”指向一个find_工具,绝不指向创建。因权限、工具集或套餐而被隐藏的工具会返回相应原因。原因:模型只能基于它能看到并理解的错误进行自我纠正。测试:契约套件中的每个错误路径都以Error:开头,并有一个可解析的JSON行。来源:MCP schema,CallToolResult.isError:工具错误“应在结果对象内报告……否则LLM无法看到发生了错误并自我纠正”;连接器审查标准拒绝通用的“内部服务器错误”响应;两者已验证于2026-10-31。 - [不可变] 文本块和
structuredContent携带相同的JSON,声明的outputSchema覆盖每个结构化结果(包括错误)。原因:不同客户端用于到达模型的通道可能不同,并且它们应该对照声明的模式进行验证。测试:对于套件中的每个结果,解析后的文本等于structuredContent。来源:MCP工具规范,具有结构化内容的工具“也应将序列化的JSON放在一个TextContent块中返回”(已验证于2026-10-31)。 - [不可变] 时间和货币是服务器的工作:时间以本地时间加IANA时区或带偏移量的ISO 8601形式给出,绝不不包含时区;不存在的和模糊的本地时间会被拒绝并给出选项;结果中的每个时间都显示工作日、带时区的本地时间、UTC以及距离现在的时间。总计、税费和舍入在服务器端使用十进制字符串或整数最小单位计算。原因:缺少工作日,差一天和夏令时的错误很难发现。测试:
2026-03-29 02:30在Europe/Berlin时区被拒绝;2026-10-25 02:30返回两个偏移量。来源:可通过上述测试重现。 - [不可变] 紧急开关放在一处:一个全局标志,通过CLI命令和管理员开关(两者都审计)设置,使所有MCP调用返回503并附带
Retry-After,同时保持连接;一个每个租户的开关,其默认值只定义一次,允许租户管理员关闭代理访问。原因:发生事故时,需要一个操作即可停止所有代理写入。测试:标志开启,调用一个写入工具;返回503,无数据行,无作业。来源:可通过上述测试重现。 - [不可变] 每个MCP写入操作都使用
mcp通道、连接和客户端名称进行审计,在入口点设置为列,并且list_recent_changes显示此连接更改了哪些内容。原因:租户可以看到代理做了什么,而失去上下文的代理可以找到其草稿,而不是创建新的。测试:每个写入工具正好产生一行审计记录,通道为mcp。来源:可通过上述测试重现。 - [不可变] MCP和API凭据不可混用:MCP令牌有其自己的类型和前缀,存储时进行哈希处理,仅显示一次,并被所有API路由拒绝;API令牌和会话cookie被MCP端点拒绝。使用OAuth时,仅接受为此服务器颁发的令牌,绝不传递令牌。验证
Origin。原因:MCP令牌以明文形式存在于客户端的配置文件中。测试:每种配对返回401;错误的Origin返回403。来源:MCP授权规范,服务器“绝不能接受或传递任何其他令牌”(https://modelcontextprotocol.io/specification/2026-07-28/server/authorization,已验证于2026-10-31)。
... (此技能在原始文档中截断)