首页 / 文章

MCP服务器失效的两种方式及防止其发生的防护机制

MCP服务器会因过宽的令牌范围及缺乏任务感知的工具而出现故障。限定范围的令牌、受众验证、经过处理的错误信息以及工具使用上限共同构成了防护层。

发布者:REACTAPP.TOP阅读约 8 分钟1896 词

来源: MCP Servers Fail in Two Ways, and Both Are Preventable. Here’s the Guardrail Layer.

大多数MCP服务器并非因崩溃而失效,而是由于在权限分配和接口设计这两个方面给予了超出任务需求之外的信任。更智能的模型无法解决这一问题,但设置在智能体与工具之间的防护层可以做到,以下内容将通过代码展示这类防护层的具体构成。

无人请求的清理操作

用户要求智能体将生产环境中的数据库复制到预生产环境中。在处理该请求时,智能体认为先进行清理操作会更整洁,于是先删除了一些旧的测试数据。实际上并没有人要求这么做。如果MCP服务器转发了用户自身的凭证,且该用户拥有管理员权限,那么删除操作就会成功执行,生产环境中的数据就会消失。

AWS在其MCP治理指南中正是以这种场景为例,解释为何不能跳过权限范围限制机制。

服务器是信任边界

人们通常会像描述其他集成工具那样来描述MCP服务器:即那种将智能体与系统连接起来的工具,提供少量功能即可完成任务。但实际上,正是在这里,模型做出的行动决策会真正产生副作用,需要借助真实的基础设施和凭证。在此之前的所有步骤都仅仅是推理过程。服务器则是推理产生实际后果的节点。

因此,无论是否经过刻意设计,服务器都属于信任边界。用这一视角审视生产环境中的服务器,其错误可归为两类。

第一类故障:权限过宽

数据库相关的问题属于常见情况。AWS的治理指南指出了解决方案:每个下游调用都应使用专门为该调用生成、具有相应作用域的令牌,而绝不能使用启动代理时的凭证。指南还要求对令牌的受众声明进行验证,从而防止为某个MCP服务器生成的令牌在另一个服务器上被重复使用。

很少有团队会尽早采取这一措施,因为相比直接传递调用方的令牌,它需要更多的配置工作。然而,这正是能够安全失败的低质量工具调用与真正投入生产环境的调用之间的关键区别。

第二类故障:糟糕的接口

第二种情况更为隐蔽。不会发生任何戏剧性的事件,只是可靠性逐渐下降,而且很难追溯原因。微软的 Azure MCP 开发指南指出了背后的习惯:服务器被设计成类似 REST API 的形式,每个操作都作为一个独立的工具呈现,而实际设计应遵循代理想要实现的目标。指南中的示例很好地说明了这一点——与其使用名为 create_ticket、add_comment、assign_ticket 和 notify_user 的四个工具,不如用一个 file_support_request 工具在内部完成所有这四步操作。

该指南指出了这种习惯的另外两个症状。描述内容可能会承诺工具无法实现的功能,因此代理会信任那些代码根本不会遵守的文本说明。而错误信息可能毫无用处(仅显示“错误”二字),或者透露过多信息:堆栈跟踪、内部文件路径,或是确认某个用户存在于数据库中的信息,这些信息本身就可能被用于枚举攻击。

怀疑方向的不对称性

试想一下,代理的最终回复背后需要多少工程努力:结构化输出验证、限制措施、重试机制以及验证步骤。模型输出确实应被视为可疑的,这也是理所当然的。

现在来看看那条回复背后的工具调用层。调用者的令牌会直接传递过去,工具列表也会随之扩展,却没人去检查模型是否仍能对这些工具进行合理处理。工具的描述会被原封不动地接受,也从未有测试显示智能体能正确使用这些工具。人们花费大量精力去质疑模型的输出,却几乎不质疑它的实际行为。

这种失衡的怀疑态度正是两类系统存在的真正缺陷。仅靠更智能的权限逻辑或更好的工具名称都无法解决这个问题。真正需要的是一个能在赋予信任之前检查每一次工具调用的层,而不仅仅是检查模型的每一条回复。

构建该层

这里的处理单元是工具调用。不要让原始请求直接到达后端,应先对其进行封装。

from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Optional

@dataclass
class ScopedToken:
 audience: str # which MCP server this token is valid for
 permissions: list[str] # e.g. ["read", "create"] - never assume "all"
 issued_at: datetime
 expires_at: datetime
 source_user: str # who originally triggered this, for audit

该封装结构中的每个字段都是为了让上一层能够判断应对该调用赋予多大的信任度。接下来是相关控制选项。

在工具运行前限定令牌的范围

切勿重复使用调用方带来的凭据来执行后续操作,而应生成一个范围更小的令牌,仅覆盖该工具即将执行的操作。

def issue_scoped_token(user_token: ScopedToken, tool_name: str,
 required_permissions: list[str]) -> ScopedToken:
 # Never grant more than the tool declares it needs
 granted = [p for p in required_permissions if p in user_token.permissions]
 if set(required_permissions) - set(granted):
 raise PermissionError(
 f"{tool_name} requires {required_permissions}, "
 f"caller only has {user_token.permissions}"
 )
 return ScopedToken(
 audience=tool_name,
 permissions=granted,
 issued_at=datetime.utcnow(),
 expires_at=datetime.utcnow() + timedelta(minutes=5),
 source_user=user_token.source_user,
 )

这就是让数据库场景能够安全失败的关键机制。无论模型如何设定,仅具有读取和创建权限的请求都绝不可能执行删除操作。

每次调用时都要检查访问权限

为某个工具生成的令牌应被所有其他工具拒绝。

def enforce_audience(token: ScopedToken, expected_tool: str) -> None:
 if token.audience != expected_tool:
 raise PermissionError(
 f"Token issued for '{token.audience}' cannot be used on '{expected_tool}'"
 )

根据任务需求定制工具

这采用了 Azure 指南中的 file_support_request 模式:

def file_support_request(customer_email: str, issue_type: str, description: str) -> dict:
 ticket = create_ticket(issue_type, description)
 add_comment(ticket.id, f"Filed by {customer_email}")
 assign_ticket(ticket.id, team=route_by_type(issue_type))
 notify_user(customer_email, ticket.id)
 return {
 "ticket_id": ticket.id,
 "status": "open",
 "assigned_team": ticket.team,
 }

一次调用只包含一个明确的意图,因此智能体无需自行处理四项操作的顺序问题。

在智能体看到之前先对错误进行净化

在服务器上记录完整细节,只向智能体提供其恢复所需的信息。

def safe_error(internal_message: str, request_id: str) -> dict:
 # internal_message goes to your logs, never to the model
 log.error(internal_message, extra={"request_id": request_id})
 return {
 "content": [{
 "type": "text",
 "text": f"Unable to complete the request. Request ID: {request_id}. "
 f"Try again or contact support."
 }],
 "isError": True,
 }

限制每台服务器上的工具数量

在投入生产之前先了解相关限制。微软给出了具体数值:每台服务器上的工具数量不应超过10到15个,这些工具需按域名分开,且不得继续在单台服务器上增加工具数量。

MAX_TOOLS_PER_SERVER = 15

def register_tool(server, tool):
 if len(server.tools) >= MAX_TOOLS_PER_SERVER:
 raise ValueError(
 f"{server.name} already has {len(server.tools)} tools. "
 f"Split into a domain-specific server instead of adding more."
 )
 server.tools.append(tool)

对现有服务器的三个快速检查

这些检查均无需重新构建服务器。

要审核的是令牌的权限范围,而不仅仅是是否存在令牌。提取最近的十次工具调用记录,将每个令牌所拥有的权限与工具实际所需的权限进行对比。如果两者通常存在差异,那就说明有超出必要的宽权限令牌在流通,这很可能就是当前的现状。

根据自身工具数量来计算令牌需求。AWS的指导标准指出,一旦服务器拥有的工具超过20个,仅描述信息一项就会消耗每次调用5,000到10,000个令牌。先统计一下你拥有的工具数量。如果服务器数量远超此数且没有进行任何过滤或拆分处理,就会出现上下文过载的问题,而这种问题仅靠修改模型是无法解决的。

尝试一种可能尚未被开展过的代理级测试。选取三种最常用的工具,使用真实的代理而非单元测试,来验证它们能否从日常的自然语言请求中选出正确的工具。微软的指南将这一层称为几乎所有人都会忽略的部分。即便通过了单元测试和协议测试,若不进行此项测试,仍无法确定这些工具是否真正有效。

其与其他部分的关联

这些措施并不会取代现有的可靠性工作,而是与之并行存在。范围限定的令牌遵循与在任意信任边界验证输入相同的原理。对工具数量的限制以及以任务为中心的设计,体现了 API 版本控制和接口设计的严谨性,只不过这里是由模型而非人类开发者来读取数据。每个 MCP 服务器都是系统决定对调用方信任程度的又一环节,因此需要像其他部分一样得到妥善处理。

团队采用 MCP 是因为它能让智能体拥有更广泛的访问能力:只需一个标准途径即可接入真实系统,而无需上百种自定义集成。问题在于,模型无论接收到范围界定不清的调用还是界定清晰的调用,都会同样忠实地执行,因为它无法感知二者之间的差异,只有防护层才能做到这一点。

权限故障与接口故障看似毫无关联,前者关乎访问权限,后者涉及设计问题。二者都源于一个未经审视的假设,即工具调用已获得了实际上并未被授予的信任。大多数人工智能系统都将怀疑焦点放在输出端,但工具调用端同样值得关注。

这就引出了对每台正在使用的MCP服务器的一个问题:目前是什么阻碍了智能体对那些被赋予过度信任的调用采取行动?如果已经如此,是否会触发警报?

简短答案

什么是MCP令牌范围限制?它指的是为每次工具调用分配专属的、范围有限的令牌,而不转发调用者原有的凭证。AWS的治理指南建议采用这种方式,以避免因底层凭证允许而导致的错误或意外操作得以实施。

一个MCP服务器应提供多少个工具?Azure的建议是通常提供10到15个,超过这个数量则应按领域拆分服务器。此外,代理选择合适工具的频率会显著降低,而AWS指出,当工具数量超过20个时,仅描述这些工具就需要每次调用支付5,000到10,000个令牌的费用。

以任务为导向的工具设计与系统API设计有何不同?系统API设计会将每个后端操作都视为独立的工具,类似于REST API。而以任务为导向的设计则将用户想要完成的任务整合为一个工具,由该工具内部处理各个步骤。微软的例子就是用一个file_support_request工具来替代原来的四个工单处理操作。

为何要净化 MCP 错误信息?堆栈跟踪、内部路径或表明某条记录存在的线索会向攻击者提供本不应获得的详细信息,这至少构成一种枚举漏洞。更安全的方法是将全部详细信息记录到服务器端日志中,仅向智能体返回其恢复所需的信息,并附上关联 ID。

单元测试能否检测到这些问题?无法检测。单元测试仅覆盖工具的逻辑,而协议测试则关注 MCP 的合规性,但两者都无法证明智能体能在实际场景中找到、选择并正确使用该工具。这需要由真实智能体驱动的第三层测试,而大多数团队都忽略了这一点。

相关阅读

  • MCP令牌成本重新测算:两种工具描述导致了成本上升 — 对九台MCP服务器进行13天后的再次测试显示,工具定义的令牌数量从36,320增加到了38,738,几乎全部来自某台服务器对其原有描述的重写。