在 MCP 的实际使用中,一个 Server 往往会注册大量工具。以 TMC 差旅支付系统为例,一个支付 MCP Server 承载着完整的账务链路——从发票创建、结算,到退款逆向流程,再到超额支付转预存款、预存款抵扣供应商 XO(Exchange Order)等。但并非每个 Agent 都需要全部工具:客户对账 Agent 只需正向发票和结算工具,供应商结算 Agent 只需 XO 抵扣和预存款工具。
Spring AI 提供了 McpToolFilter 机制,允许在 Client 侧按需过滤工具,本文从原理到实践完整讲解。
一、为什么要做工具过滤?
在企业级场景中,MCP Server 的工具数量随着业务增长会持续膨胀。如果不加过滤地全部暴露给大模型,会带来三个核心问题:
1. Token 成本与上下文压力:每个工具都带有结构化的 Schema 和详细描述,这些内容会塞进系统提示词中占用上下文窗口。如果一个 MCP Server 有 30 个工具,而当前任务只需要其中 3 个,其余 27 个工具的描述就成为纯粹的 token 浪费,同时挤占了模型可处理的有效信息空间。 2. 工具选择准确性下降:工具数量越多,模型在决策调用哪个工具时需要遍历的候选集就越大。干扰项越多,选错工具或产生幻觉的概率越高。过滤掉无关工具后,模型在更干净的候选集中做决策,调用成功率显著提升。 3. 多智能体角色隔离:在多 Agent 架构中,不同 Agent 有明确的职责边界。例如,TMC 系统中"客户对账 Agent"只需要发票创建、结算、退款等客户侧工具,而"供应商结算 Agent"只需要 XO 查询、预存款抵扣等供应商侧工具。不做过滤的话,所有 Agent 都看到全部工具,不仅增加复杂度,还可能造成误调用甚至越权操作。工具过滤为每个 Agent 提供精确的工具可见性,确保职责清晰、边界明确。
因此,在 Client 侧对 MCP Server 工具做过滤,只将当前场景需要的工具注入给模型,是简洁且有效的工程实践。Spring AI 提供了 McpToolFilter 来实现这一点。
二、工具过滤原理
2.1 过滤发生的时机
工具过滤发生在 SyncMcpToolCallbackProvider.getToolCallbacks() 方法中——也就是将 McpSyncClient 中的工具列表转换为 ToolCallback 列表的时候。核心链路如下:
public ToolCallback[] getToolCallbacks() {
if (this.invalidateCache) { // 检查是否需要刷新缓存
this.lock.lock();
try {
if (this.invalidateCache) { // 双重检查锁定
this.cachedToolCallbacks = this.mcpClients.stream()
.flatMap(mcpClient ->
mcpClient.listTools().tools().stream()
.filter(tool -> this.toolFilter.test(
mcpConnectionInfo, tool)) // 过滤点
.map(tool -> SyncMcpToolCallback.builder()
.mcpClient(mcpClient)
.tool(tool)
.build())
)
.toList();
this.validateToolCallbacks(this.cachedToolCallbacks);
this.invalidateCache = false;
}
} finally {
this.lock.unlock();
}
}
returnthis.cachedToolCallbacks.toArray(newToolCallback[0]);
}关键点:this.toolFilter.test(mcpConnectionInfo, tool) 对每个工具执行判断,返回 true 保留,false 丢弃。过滤后的工具列表会被缓存,避免每次请求都重新拉取远端工具列表。
2.2 McpToolFilter 的本质
package org.springframework.ai.mcp;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.function.BiPredicate;
publicinterfaceMcpToolFilter
extendsBiPredicate<McpConnectionInfo, McpSchema.Tool> {
}McpToolFilter 继承自 BiPredicate<McpConnectionInfo, McpSchema.Tool>,本质是一个接收两个参数、返回 boolean 的函数式接口:
• 参数一 McpConnectionInfo:MCP 连接信息,包含 Server 的 capabilities、名称、版本等• 参数二 McpSchema.Tool:单个工具的定义,包含name、description、inputSchema、annotations等元数据
因为它是 BiPredicate(@FunctionalInterface),可以直接用 Lambda 表达式构造。Spring AI 的默认行为是全放行——(mcpClient, tool) -> true,所有工具不加过滤地暴露给模型。
三、实操:TMC 差旅账务工具过滤
3.1 场景设定
TMC 差旅支付的完整账务链路如下:
1. 正向流程:客户下单 → 创建发票(Invoice)→ 结算(Settle),单据闭环 2. 逆向流程:结算后发生退票,走退款(Refund) 3. 异常流程:结算时多刷(OverPayment),超出部分自动转入预存款(Deposit) 4. 核销流程:预存款可用于抵扣供应商 XO(Exchange Order)
基于这套流程,TMC 差旅支付 MCP Server 注册了以下 7 个工具:
payment_createInvoice | |||
payment_settleInvoice | |||
payment_refundOrder | |||
deposit_handleOverPayment | |||
deposit_queryBalance | |||
supplier_offsetWithDeposit | |||
supplier_queryExchangeOrder |
现在要构建一个客户对账 Agent,它负责客户侧的完整账务——从发票创建、结算、到退款和超额处理,但不需要供应商侧的 XO 核销工具。按命名规范,客户侧工具统一以 payment_ 为前缀,恰好覆盖了代理需要的 3 个核心工具。deposit_handleOverPayment 虽然属于 deposit 前缀,但它是结算超额后的自动流程,可以按需通过多条件过滤加入(见第四节)。
3.2 Server 端注册工具
@Service
@Slf4j
publicclassTmcPaymentService {
// 正向流程:Invoice → Settle
@Tool(name = "payment_createInvoice",
description = "根据差旅订单为客户创建发票")
public String createInvoice(String orderId, double amount) {
return String.format("发票已创建:订单 %s,金额 ¥%.2f,待结算", orderId, amount);
}
@Tool(name = "payment_settleInvoice",
description = "结算客户发票,校验金额并完成支付。"
+ "若实际支付金额大于发票金额,将标记为超额支付(OverPayment)")
public String settleInvoice(String invoiceId, double paidAmount) {
return String.format("发票 %s 已结算,实付 ¥%.2f,%s",
invoiceId, paidAmount,
paidAmount > getInvoiceAmount(invoiceId)
? "超额部分将转入预存款" : "结算完成");
}
// 逆向流程:Refund
@Tool(name = "payment_refundOrder",
description = "对已结算的差旅订单发起退款")
public String refundOrder(String orderId, String reason) {
return String.format("退款已受理:订单 %s,原因:%s,将原路返回", orderId, reason);
}
// 异常流程:OverPayment → Deposit
@Tool(name = "deposit_handleOverPayment",
description = "处理结算超额支付,将多余金额转入客户预存款")
public String handleOverPayment(String invoiceId, double overAmount) {
return String.format("超额 ¥%.2f 已转入预存款,发票 %s", overAmount, invoiceId);
}
@Tool(name = "deposit_queryBalance",
description = "查询客户的差旅预存款余额")
public String queryBalance(String customerId) {
return String.format("客户 %s 预存款余额:¥%.2f", customerId, Math.random() * 50000);
}
// 核销流程:Deposit → Supplier XO
@Tool(name = "supplier_offsetWithDeposit",
description = "使用客户预存款余额抵扣供应商 XO(Exchange Order)")
public String offsetSupplierXO(String customerId, String xoId, double amount) {
return String.format("预存款抵扣成功:客户 %s,XO %s,抵扣金额 ¥%.2f",
customerId, xoId, amount);
}
@Tool(name = "supplier_queryExchangeOrder",
description = "查询供应商 XO(Exchange Order)的签发状态与可抵扣金额")
public String queryExchangeOrder(String xoId) {
return String.format("XO %s:已签发,可抵扣金额 ¥%.2f,有效期至 2026-12-31",
xoId, Math.random() * 100000);
}
privatedoublegetInvoiceAmount(String invoiceId) {
return1000.0; // 模拟
}
}3.3 Client 端配置过滤器
在构建 SyncMcpToolCallbackProvider 时传入 toolFilter,使用 startsWith("payment_") 只保留客户侧工具:
// 创建 MCP 客户端连接
HttpClientStreamableHttpTransporttransport=
HttpClientStreamableHttpTransport
.builder("http://127.0.0.1:8004/stream/test/")
.endpoint("api/mcp")
.build();
McpSyncClientmcpClient= McpClient.sync(transport)
.clientInfo(newMcpSchema.Implementation("customer-billing-agent", "1.0"))
.requestTimeout(Duration.ofSeconds(10))
.build();
mcpClient.initialize();
List<McpSyncClient> clients = List.of(mcpClient);
// 关键:通过 toolFilter 只保留 payment_ 开头的客户侧工具
SyncMcpToolCallbackProviderprovider=
SyncMcpToolCallbackProvider.builder()
.mcpClients(clients)
.toolFilter((conn, tool) ->
tool.name().startsWith("payment_"))
.build();
ToolCallback[] callbacks = provider.getToolCallbacks();
// callbacks 只包含 3 个工具:
// payment_createInvoice、payment_settleInvoice、payment_refundOrder
// deposit_ 和 supplier_ 开头的 4 个工具被过滤掉
this.chatClient = ChatClient.builder(chatModel)
.defaultToolCallbacks(callbacks)
.build();运行后,provider.getToolCallbacks() 只返回 3 个 payment_ 开头的工具。客户对账 Agent 拥有完整的正向(Invoice → Settle)和逆向(Refund)链路工具,而供应商侧的 XO 核销工具完全不可见。模型在做工具选择时候选集更干净、决策更准确。
四、常用过滤策略
除了 startsWith 前缀匹配,实际项目中还有几种常用的过滤方式:
按工具名前缀(推荐):约定命名规范,payment_(客户侧)、supplier_(供应商侧)、deposit_(预存款)等前缀天然区分业务域。
// 客户对账 Agent:只暴露客户侧工具
.toolFilter((conn, tool) -> tool.name().startsWith("payment_"))按 annotation 标记过滤:在 MCP Server 端为工具打上业务分类标签,Client 端按标签筛选,比前缀更灵活。
// Server 端:标注业务分类
@McpTool(name = "payment_createInvoice",
annotations = { @McpToolAnnotation(key = "category", value = "customer") })
@McpTool(name = "supplier_offsetWithDeposit",
annotations = { @McpToolAnnotation(key = "category", value = "supplier") })
// Client 端:只暴露客户类工具
.toolFilter((conn, tool) -> {
varann= tool.annotations();
return ann != null
&& "customer".equals(ann.annotations().get("category"));
})前缀方式适合业务边界清晰、命名规范统一的场景;annotation 方式适合工具职责存在交叉、需要更灵活打标的场景。
多条件组合:假设客户对账 Agent 不仅需要 payment_ 前缀的工具,还需要 deposit_queryBalance 来查询预存款余额:
.toolFilter((conn, tool) -> {
Stringname= tool.name();
return name.startsWith("payment_")
|| "deposit_queryBalance".equals(name);
})按 Server 来源 + 前缀组合:当 Agent 连接了多个 MCP Server 时,按来源和工具名双重过滤。
.toolFilter((conn, tool) -> {
StringserverName= conn.initializeResult().serverInfo().name();
return tool.name().startsWith("payment_")
&& "tmc-payment-server".equals(serverName);
})白名单:维护一个精确允许的工具集合,适合工具数量少、变更不频繁的场景。
Set<String> allowed = Set.of(
"payment_createInvoice",
"payment_settleInvoice",
"deposit_queryBalance"
);
.toolFilter((conn, tool) -> allowed.contains(tool.name()))选择哪种策略取决于团队的命名规范和治理方式。推荐优先使用命名前缀 + annotation 标记的组合——命名前缀提供粗粒度的业务域划分,annotation 提供细粒度的工具级控制。
五、总结
1. 为什么过滤:减少 token 浪费、提升模型工具选择准确率、实现多 Agent 角色隔离(如客户对账 Agent vs 供应商结算 Agent) 2. 过滤原理: SyncMcpToolCallbackProvider在构建ToolCallback列表时,通过McpToolFilter(本质是BiPredicate<McpConnectionInfo, McpSchema.Tool>)对每个工具做 test3. 实现方式:在 builder 中传入 Lambda 表达式,最常用的是 tool.name().startsWith("prefix_")——良好的工具命名规范(如payment_、supplier_、deposit_)本身就构成天然的过滤维度4. 进阶策略:annotation 标记、白名单、多条件组合均可,根据团队治理规范选择。推荐前缀 + annotation 组合——前缀粗粒度划域,annotation 细粒度控制
工具过滤是 MCP 工程化落地的基础操作——它让每个 Agent 拥有精确的工具视野,既节省成本又提升可靠性。
夜雨聆风