Spring AI 2.0 MCP 协议实战:Model Context Protocol SDK 与多服务器编排
关键词:MCP、Model Context Protocol、跨系统互操作、负载均衡、故障转移
做 AI 应用落地的同学应该都经历过这种痛苦:不同大模型厂商的工具调用协议各搞一套,OpenAI 有 function calling,Claude 有 tool use,Gemini 又有自己的 declarative tools。你要接三个模型,就得写三套工具适配代码。更别提企业内部那一堆"AI 能力孤岛"------财务系统的智能报表、CRM 的客户洞察、知识库的语义检索------每个都是独立的服务,想给大模型串联起来调用,全靠手写胶水代码。
Anthropic 在 2024 年底推出的 Model Context Protocol(MCP) 就是冲着这个问题来的。它定义了一套标准化协议,让 AI 模型能以统一的方式与外部工具和资源交互。Spring AI 作为 MCP 生态的早期贡献者(直接参与了官方 MCP Java SDK 的开发和维护),在 2.0.0-M2 中把 MCP SDK 升级到了 0.17.2 ,同时引入了 McpSyncServerCustomizer、McpAsyncServerCustomizer 等关键接口,修复了非 Web 上下文下的自动配置问题。
这篇文章,我会从架构师视角把 Spring AI 的 MCP 集成掰开揉碎讲清楚,重点覆盖多服务器编排、负载均衡和故障转移的实战方案。
一、MCP 协议核心概念
1.1 MCP 解决什么问题
先看一张架构对比图,传统方案 vs MCP 方案:
传统方案(每个模型一套适配):
+----------+ +-----------+ +----------+
| OpenAI |---->| 适配器 A |---->| 财务系统 |
+----------+ +-----------+ +----------+
+----------+ +-----------+ +----------+
| Claude |---->| 适配器 B |---->| 财务系统 |
+----------+ +-----------+ +----------+
+----------+ +-----------+ +----------+
| Gemini |---->| 适配器 C |---->| 财务系统 |
+----------+ +-----------+ +----------+
问题:3个模型 x N个工具 = 3N 套适配代码
MCP 方案(统一协议层):
+----------+ +------------------+
| OpenAI | | |
+----------+ | |
+----------+ | MCP 客户端层 | +----------+
| Claude |---->| (统一协议处理) |---->| 财务系统 |
+----------+ | | +----------+
+----------+ | | +----------+
| Gemini | | |---->| CRM系统 |
+----------+ +------------------+ +----------+
| |
+--------+ +--------+
| |
+---------+ +---------+
| MCP Srv | | MCP Srv |
| (本地) | | (远程) |
+---------+ +---------+
优势:模型无关,工具一次接入处处可用
MCP 的核心思想很简单:把"AI 模型调用工具"这件事标准化。它定义了四个基础能力:
| 能力 | 说明 | 典型场景 |
|---|---|---|
| Tools(工具) | 模型可调用的函数 | 数据库查询、API调用、文件操作 |
| Resources(资源) | 模型可读取的数据 | 配置文件、文档、数据库记录 |
| Prompts(提示) | 预定义的提示模板 | 代码审查模板、日报生成模板 |
| Sampling(采样) | 服务器反调模型的能力 | 工具执行结果需要模型二次判断 |
1.2 三层架构
MCP Java SDK 采用了清晰的三层设计:
+------------------------------------------------------+
| Client / Server 层 |
| McpClient | McpServer |
+------------------------------------------------------+
| Session 层 |
| McpSession | McpClientSession | McpServerSession |
+------------------------------------------------------+
| Transport 层 |
| StdioTransport | SseTransport | HttpTransport |
+------------------------------------------------------+
| |
+-------+ +-------+
| |
标准输入/输出 HTTP 通信
(进程内通信) (远程通信)
Transport 层负责 JSON-RPC 消息的序列化/反序列化和传输。Spring AI 目前支持三种传输方式:
- Stdio:通过标准输入输出流通信,适用于本地进程场景。比如你在 Java 应用中内嵌一个 Node.js 编写的 MCP Server。
- SSE(Server-Sent Events):基于 HTTP 长连接的流式通信,适合远程服务。
- Streamable HTTP:MCP SDK 0.17.2 中引入的新传输方式,支持可流式传输的 HTTP 请求,在保持无状态的同时提供流式响应能力。
1.3 业务场景:为什么你需要 MCP
举一个真实的企业场景。某电商平台需要构建智能客服系统,AI 需要同时访问:
- 订单系统(Java 微服务)------ 查询订单状态
- 商品知识库(Elasticsearch)------ 检索商品信息
- 退换货流程引擎(Python 服务)------ 执行退换货操作
- 内部文档库(Confluence)------ 查询售后政策
没有 MCP 之前,你要么写一个庞大的网关服务统一代理这些能力,要么让每个 AI 模型分别对接。有了 MCP,每个系统只需要实现一个 MCP Server,AI 客户端通过统一协议就能调用所有能力。
二、Spring AI MCP SDK 详解
2.1 MCP SDK 0.17.2 升级要点
Spring AI 2.0.0-M2 将底层 MCP Java SDK 从之前的版本升级到了 0.17.2,这个版本的升级包含几个关键变化:
新增 McpSyncServerCustomizer 和 McpAsyncServerCustomizer 接口
之前版本中,MCP Server 的定制能力比较有限,只能通过配置属性调整。0.17.2 引入了专门的定制器接口:
java
/**
* 同步 MCP 服务器定制器
* 用于在服务器构建阶段对 McpSyncServer.Spec 进行自定义
*/
public interface McpSyncServerCustomizer {
void customize(McpSyncServer.Spec spec);
}
/**
* 异步 MCP 服务器定制器
* 用于在服务器构建阶段对 McpAsyncServer.Spec 进行自定义
*/
public interface McpAsyncServerCustomizer {
void customize(McpAsyncServer.Spec spec);
}
这两个接口的意义在于:你可以通过注册 Spring Bean 的方式,在 MCP Server 启动前注入自定义逻辑,比如修改超时配置、注入自定义 capabilities、调整工具注册策略等。
非 Web 应用上下文下的自动配置修复
这是一个重要的 bug fix。之前的版本中,如果你在一个非 Web 的 Spring Boot 应用(比如命令行工具、批处理任务)中使用 MCP Client,自动配置可能会因为缺少 Web 上下文而失败。2.0.0-M2 修复了这个问题,MCP 的自动配置现在可以正确地在非 Web 应用上下文中工作。
这意味着你现在可以这样用:
java
@SpringBootApplication
public class McpConsoleApplication implements CommandLineRunner {
@Autowired
private ChatClient chatClient;
@Autowired
private SyncMcpToolCallbackProvider toolCallbackProvider;
public static void main(String[] args) {
SpringApplication.run(McpConsoleApplication.class, args);
}
@Override
public void run(String... args) {
// 非Web环境下也能正常使用MCP工具
String result = chatClient.prompt()
.user("查询北京今天的天气")
.toolCallbacks(toolCallbackProvider.getToolCallbacks())
.call()
.content();
System.out.println(result);
}
}
2.2 依赖管理
先看 Maven 依赖。Spring AI 的 MCP 集成分为 Client 和 Server 两个维度,各自有对应的 starter:
xml
<dependencies>
<!-- MCP 客户端 - 连接外部 MCP 服务器 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<!-- MCP 服务器 - 暴露本应用的工具能力 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<!-- 如果需要响应式传输,用 webflux 版本 -->
<!--
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
</dependency>
-->
</dependencies>
版本管理建议通过 BOM 引入:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0-M2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Spring AI 仓库需要单独添加:
xml
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
2.3 MCP Client 配置详解
MCP Client 的配置前缀是 spring.ai.mcp.client,下面分几个层面看。
基础配置:
yaml
spring:
ai:
mcp:
client:
enabled: true # 启用/禁用 MCP 客户端
type: SYNC # 客户端类型:SYNC 或 ASYNC
request-timeout: 30s # 请求超时时间,默认 20s
toolcallback:
enabled: true # 是否将 MCP 工具注册为 Spring AI ToolCallback
type 配置决定了底层使用同步还是异步客户端。对于大多数 I/O 密集型的 AI 应用,SYNC 模式已经足够,且更容易调试。如果你的应用本身就是 WebFlux 架构,可以用 ASYNC 模式获得更好的资源利用率。
Stdio 连接配置:
Stdio 是通过启动子进程进行通信的方式,适合接入本地工具:
yaml
spring:
ai:
mcp:
client:
stdio:
connections:
filesystem:
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "/data/documents"
env:
NODE_ENV: production
brave-search:
command: npx
args:
- "-y"
- "@modelcontextprotocol/server-brave-search"
env:
BRAVE_API_KEY: ${BRAVE_API_KEY}
Windows 环境注意 :Windows 下执行 npx 需要用 cmd.exe /c 包装:
yaml
spring:
ai:
mcp:
client:
stdio:
connections:
filesystem:
command: cmd.exe
args:
- "/c"
- "npx.cmd"
- "-y"
- "@modelcontextprotocol/server-filesystem"
- "D:\\data\\documents"
SSE 连接配置:
SSE 适合连接远程 MCP 服务器:
yaml
spring:
ai:
mcp:
client:
sse:
connections:
order-service:
url: https://order-service.internal:8081
sse-endpoint: /mcp/sse # 默认值 /sse
crm-service:
url: https://crm-service.internal:8082
sse-endpoint: /sse
Streamable HTTP 连接配置:
yaml
spring:
ai:
mcp:
client:
streamable-http:
connections:
knowledge-base:
url: https://kb-service.internal:8083
endpoint: /mcp # 默认值 /mcp
混合连接配置示例------实际生产中你往往需要同时连接本地和远程的 MCP 服务器:
yaml
spring:
ai:
mcp:
client:
enabled: true
type: SYNC
request-timeout: 30s
stdio:
connections:
local-fs:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data"]
sse:
connections:
order-mcp:
url: https://order-svc:8081
streamable-http:
connections:
knowledge-mcp:
url: https://kb-svc:8083
2.4 自定义 Client 配置
Spring AI 提供了四个核心扩展点来自定义 MCP Client 的行为:
McpSyncClientCustomizer / McpAsyncClientCustomizer
这两个接口分别用于定制同步和异步客户端。它们的回调方法接收两个参数:连接配置名称(serverConfigurationName)和客户端构建规范(McpClient.SyncSpec / McpClient.AsyncSpec)。
java
@Component
public class CustomMcpSyncClientCustomizer implements McpSyncClientCustomizer {
private static final Logger log = LoggerFactory.getLogger(CustomMcpSyncClientCustomizer.class);
@Override
public void customize(String serverConfigurationName, McpClient.SyncSpec spec) {
log.info("Customizing MCP client for server: {}", serverConfigurationName);
// 针对不同服务器设置不同超时
if ("slow-external-api".equals(serverConfigurationName)) {
spec.requestTimeout(Duration.ofSeconds(60));
}
// 注册工具变更监听
spec.toolsChangeConsumer(tools -> {
log.info("Tools changed for [{}], current tools: {}",
serverConfigurationName,
tools.stream().map(t -> t.name()).toList());
});
// 注册日志消费
spec.loggingConsumer(logEntry -> {
log.info("[MCP Log][{}] {}", serverConfigurationName, logEntry);
});
// 设置根路径(文件系统访问边界)
spec.roots(List.of(new Roots.Root(
"data-root", "file:///data/documents", "Shared documents"
)));
}
}
java
@Component
public class CustomMcpAsyncClientCustomizer implements McpAsyncClientCustomizer {
@Override
public void customize(String serverConfigurationName, McpClient.AsyncSpec spec) {
spec.requestTimeout(Duration.ofSeconds(30));
// 异步模式下的变更消费返回 Mono
spec.toolsChangeConsumer(tools ->
Mono.fromRunnable(() ->
System.out.println("Async tools updated: " +
tools.stream().map(McpSchema.Tool::name).toList())
)
);
}
}
McpToolFilter------工具过滤
当一个 MCP Server 暴露了太多工具时,你可能只需要其中一部分。McpToolFilter 就是干这个的:
java
@Component
public class SecurityToolFilter implements McpToolFilter {
// 只允许白名单工具通过
private static final Set<String> ALLOWED_TOOLS = Set.of(
"query_orders", "get_order_detail", "search_products",
"read_file", "list_directory"
);
@Override
public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) {
String toolName = tool.name();
String serverName = connectionInfo.serverName();
// 生产环境禁止文件写入类工具
if (toolName.contains("write") || toolName.contains("delete")
|| toolName.contains("remove")) {
log.warn("Tool [{}] from server [{}] blocked by security policy",
toolName, serverName);
return false;
}
return ALLOWED_TOOLS.contains(toolName);
}
}
注意:McpToolFilter 在应用上下文中只能定义一个 Bean,它会作用于所有 MCP 连接。
McpToolNamePrefixGenerator------命名冲突解决
当你连接多个 MCP 服务器时,不同服务器可能暴露同名工具。Spring AI 默认使用 DefaultMcpToolNamePrefixGenerator 自动添加服务器名前缀来消除冲突:
java
// 默认行为:server1 的 query 工具 → "server1__query"
// server2 的 query 工具 → "server2__query"
// 自定义前缀生成策略
@Component
public class CustomPrefixGenerator implements McpToolNamePrefixGenerator {
@Override
public String generatePrefix(String connectionName) {
// 用更简洁的前缀格式
return connectionName + "_";
}
}
// 禁用前缀(不推荐,可能导致工具名冲突)
// McpToolNamePrefixGenerator.noPrefix()
ToolContextToMcpMetaConverter
Spring AI 的 ToolContext 可以携带调用上下文信息,这个转换器负责把它传递给 MCP 工具调用的元数据:
java
@Component
public class TenantAwareMetaConverter implements ToolContextToMcpMetaConverter {
@Override
public Map<String, Object> convert(ToolContext toolContext) {
Map<String, Object> meta = new HashMap<>();
// 将租户信息注入 MCP 元数据
toolContext.get("tenantId")
.ifPresent(tenantId -> meta.put("X-Tenant-Id", tenantId));
toolContext.get("requestId")
.ifPresent(requestId -> meta.put("X-Request-Id", requestId));
return meta;
}
}
三、MCP Server 与 Tool Calling 桥接
3.1 注解驱动的 MCP Server
Spring AI 提供了一套完整的注解来声明 MCP Server 的能力,让工具暴露变得非常直观:
java
@Component
public class OrderMcpTools {
private final OrderService orderService;
public OrderMcpTools(OrderService orderService) {
this.orderService = orderService;
}
@McpTool(name = "query_orders", description = "根据条件查询订单列表")
public List<OrderSummary> queryOrders(
@McpToolParam(description = "客户ID", required = true) String customerId,
@McpToolParam(description = "订单状态:PENDING/SHIPPED/DELIVERED") String status,
@McpToolParam(description = "页码,从1开始", required = true) int page) {
return orderService.findByCustomerAndStatus(customerId, status, page)
.stream()
.map(o -> new OrderSummary(o.getId(), o.getStatus(), o.getAmount()))
.toList();
}
@McpTool(name = "get_order_detail", description = "获取订单详细信息")
public OrderDetail getOrderDetail(
@McpToolParam(description = "订单ID", required = true) String orderId) {
Order order = orderService.findById(orderId);
return new OrderDetail(
order.getId(), order.getCustomerName(),
order.getItems(), order.getTotalAmount(),
order.getShippingAddress(), order.getCreatedAt()
);
}
@McpTool(name = "cancel_order", description = "取消订单")
public CancelResult cancelOrder(
@McpToolParam(description = "订单ID", required = true) String orderId,
@McpToolParam(description = "取消原因") String reason) {
orderService.cancel(orderId, reason);
return new CancelResult(true, "订单已取消");
}
// --- 资源暴露 ---
@McpResource(uri = "order://policy/{type}", name = "退换货政策")
public String getReturnPolicy(String type) {
return policyService.getByType(type);
}
// --- 提示模板 ---
@McpPrompt(name = "order-summary-template")
public String orderSummaryPrompt(
@McpToolParam(description = "订单ID") String orderId) {
return "请为订单 %s 生成一份简洁的客户通知,包含:"
+ "1. 订单内容摘要 2. 当前状态 3. 预计送达时间".formatted(orderId);
}
}
3.2 MCP Server 配置
yaml
spring:
ai:
mcp:
server:
type: SYNC # 服务器类型:SYNC 或 ASYNC
stdio: false # 如果不需要 stdio 传输
protocol: SSE # 传输协议:SSE / STREAMABLE / STATELESS
server-info:
name: order-mcp-server
version: 1.0.0
annotation-scanner:
enabled: true # 自动扫描注解
如果你需要在服务器启动时注入自定义逻辑,使用新增的定制器接口:
java
@Component
public class OrderMcpServerCustomizer implements McpSyncServerCustomizer {
@Override
public void customize(McpSyncServer.Spec spec) {
spec.serverInfo("order-mcp-server", "1.0.0")
.capabilities(ServerCapabilities.builder()
.tools(true) // 暴露工具能力
.resources(true) // 暴露资源能力
.prompts(true) // 暴露提示能力
.logging() // 支持日志
.build());
}
}
3.3 客户端注解支持
Spring AI MCP Client 侧也提供了一组注解,用于处理服务器发起的反向调用:
java
@Component
public class McpClientHandlers {
/**
* 接收 MCP 服务器的日志消息
* 当工具执行过程中调用日志接口时触发
*/
@McpLogging
public void handleLogging(McpSyncClientExchange exchange, String level, String message) {
System.out.println("[MCP Log] " + level + ": " + message);
}
/**
* 处理采样请求
* 服务器在工具执行中需要 LLM 辅助判断时调用
*/
@McpSampling
public McpSchema.CreateMessageResult handleSampling(
McpSyncClientExchange exchange,
McpSchema.CreateMessageRequest request) {
// 将采样请求转发给本地模型
return localModel.generate(request);
}
/**
* 处理启发式请求(Elicitation)
* 服务器需要向用户获取额外信息时调用
*/
@McpElicitation
public McpSchema.ElicitResult handleElicitation(
McpSyncClientExchange exchange,
McpSchema.ElicitRequest request) {
// 返回用户确认信息
return new McpSchema.ElicitResult(
McpSchema.ElicitResult.Action.ACCEPT, Map.of());
}
/**
* 进度通知处理
* 长时间运行的工具执行时发送进度更新
*/
@McpProgress
public void handleProgress(
McpSyncClientExchange exchange,
McpSchema.ProgressNotification progress) {
System.out.printf("[Progress] %s: %d%%\n",
progress.progressToken(), progress.progress());
}
/**
* 监听工具列表变更
* 当 MCP 服务器动态添加/移除工具时触发
*/
@McpToolListChanged
public void onToolsChanged(McpSyncClientExchange exchange) {
System.out.println("Server tools list changed, refreshing...");
}
/**
* 监听资源列表变更
*/
@McpResourceListChanged
public void onResourcesChanged(McpSyncClientExchange exchange) {
System.out.println("Server resources list changed");
}
/**
* 监听提示列表变更
*/
@McpPromptListChanged
public void onPromptsChanged(McpSyncClientExchange exchange) {
System.out.println("Server prompts list changed");
}
}
3.4 桥接机制:从 MCP 工具到 Spring AI Tool Calling
这是 Spring AI MCP 集成的核心------当 spring.ai.mcp.client.toolcallback.enabled=true(默认值)时,所有从 MCP Server 发现的工具会自动注册为 Spring AI 的 ToolCallback,可以直接在 ChatClient 中使用:
MCP Server A Spring AI ChatClient
+------------------+ +------------------+
| 工具: query_db | 自动桥接 | toolCallbacks() |
| 工具: call_api | ==============> | |
| 工具: read_file | ToolCallback | LLM 决策调用 |
+------------------+ Provider | 哪些工具 |
+------------------+
MCP Server B |
+------------------+ |
| 工具: search | v
| 工具: analyze | =======> LLM 看到的是一个统一
+------------------+ 的工具列表,不关心
工具来自哪个 Server
java
@Configuration
public class ChatClientConfig {
@Bean
public ChatClient chatClient(
ChatModel chatModel,
SyncMcpToolCallbackProvider toolCallbackProvider) {
// MCP 工具自动桥接为 Spring AI ToolCallback
ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();
return ChatClient.builder(chatModel)
.defaultToolCallbacks(toolCallbacks)
.build();
}
}
调用方式跟普通的 Tool Calling 一模一样,LLM 会自动决定调用哪些工具:
java
@Service
public class IntelligentCustomerService {
private final ChatClient chatClient;
public IntelligentCustomerService(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String handleCustomerQuery(String customerMessage, String customerId) {
return chatClient.prompt()
.system("""
你是一个电商客服助手。
当前客户ID: {customerId}
你可以使用订单查询、商品搜索、退换货等工具来帮助客户。
请根据客户的问题选择合适的工具调用。
""")
.user(customerMessage)
.toolCallbacks() // 使用默认注册的 MCP 工具
.advisors(new ToolContext(Map.of("customerId", customerId)))
.call()
.content();
}
}
四、多服务器编排
4.1 多服务器连接架构
实际业务中,你往往需要同时连接多个 MCP 服务器。Spring AI 的配置天然支持多服务器连接:
yaml
spring:
ai:
mcp:
client:
enabled: true
type: SYNC
request-timeout: 30s
stdio:
connections:
# 本地文件系统服务
local-fs:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data/docs"]
sse:
connections:
# 订单微服务
order-service:
url: https://order-svc.internal:8081
sse-endpoint: /mcp/sse
# CRM 客户管理服务
crm-service:
url: https://crm-svc.internal:8082
streamable-http:
connections:
# 知识库检索服务
knowledge-base:
url: https://kb-svc.internal:8083
启动后,所有服务器的工具会统一注册到 SyncMcpToolCallbackProvider:
java
@Component
public class ToolRegistryInspector implements CommandLineRunner {
private final SyncMcpToolCallbackProvider toolProvider;
public ToolRegistryInspector(SyncMcpToolCallbackProvider toolProvider) {
this.toolProvider = toolProvider;
}
@Override
public void run(String... args) {
ToolCallback[] callbacks = toolProvider.getToolCallbacks();
System.out.println("=== Registered MCP Tools ===");
for (ToolCallback cb : callbacks) {
System.out.printf(" - %s: %s%n",
cb.getName(), cb.getDescription());
}
}
}
输出类似:
=== Registered MCP Tools ===
- local-fs__read_file: Read the contents of a file
- local-fs__list_directory: List directory contents
- order-service__query_orders: 查询订单列表
- order-service__get_order_detail: 获取订单详情
- crm-service__get_customer_info: 获取客户信息
- crm-service__search_customers: 搜索客户
- knowledge-base__semantic_search: 语义搜索
- knowledge-base__get_document: 获取文档内容
注意前缀:local-fs__、order-service__、crm-service__、knowledge-base__,这是 McpToolNamePrefixGenerator 默认生成的,自动解决了跨服务器的命名冲突。
4.2 按场景选择传输方式
不是所有服务器都必须用同一种传输方式。根据实际场景选择最合适的传输:
选择决策树:
服务器在哪里?
|
+-- 本地进程(同机部署) --> Stdio
| 场景:文件系统工具、本地脚本、npx 包
| 优势:零网络开销,启动快
|
+-- 远程服务(跨网络部署)
|
+-- 需要服务器主动推送事件? --> SSE
| 场景:实时日志、进度通知、工具变更推送
| 优势:支持双向通信,服务器可主动通知
|
+-- 纯请求-响应模式? --> Streamable HTTP
场景:知识库查询、订单查询
优势:无状态,天然支持负载均衡
4.3 特殊参数注入
MCP 工具方法可以注入一些特殊参数来获取请求上下文:
java
@Component
public class AdvancedMcpTools {
/**
* 注入 McpMeta 获取调用元数据
*/
@McpTool(name = "advanced_query", description = "高级查询,支持上下文感知")
public QueryResult advancedQuery(
@McpToolParam(description = "查询关键词", required = true) String keyword,
@McpToolParam(description = "查询范围") String scope,
McpMeta meta) { // 自动注入调用元数据
// 获取调用来源信息
String clientName = meta.get("_clientName");
String requestId = meta.get("_requestId");
log.info("Query from client [{}], request [{}]: keyword={}, scope={}",
clientName, requestId, keyword, scope);
return doQuery(keyword, scope);
}
/**
* 注入 McpSyncServerExchange 获取完整服务端上下文
*/
@McpTool(name = "contextual_operation", description = "上下文感知操作")
public OperationResult contextualOp(
@McpToolParam(description = "操作类型") String operation,
McpSyncServerExchange exchange) { // 注入服务端交换上下文
// 可以访问底层传输、会话信息
var transport = exchange.transport();
var session = exchange.session();
return new OperationResult(operation + " executed with session: "
+ session.getId());
}
/**
* 长时间运行的任务,使用进度通知
*/
@McpTool(name = "batch_process", description = "批量处理任务")
public BatchResult batchProcess(
@McpToolParam(description = "处理项列表", required = true) List<String> items,
@McpProgressToken String progressToken, // 注入进度令牌
McpSyncServerExchange exchange) {
for (int i = 0; i < items.size(); i++) {
processItem(items.get(i));
// 发送进度更新
if (progressToken != null) {
exchange.sendProgress(new McpSchema.ProgressNotification(
progressToken,
(double) (i + 1) / items.size() * 100,
(i + 1) + "/" + items.size()
));
}
}
return new BatchResult(true, items.size() + " items processed");
}
}
五、负载均衡与故障转移
5.1 为什么需要多服务器编排
在企业级场景中,单个 MCP 服务器往往会成为性能瓶颈或单点故障。举个例子:
问题场景:知识库检索服务
+------------------+ +------------------+
| AI 客户端 (1000) |--------->| MCP KB Server |
+------------------+ | (单实例,CPU打满) |
+------------------+
风险:
1. 单点故障:KB Server 挂了,所有 AI 能力失效
2. 性能瓶颈:高峰期延迟飙升
3. 扩容困难:无法水平扩展
5.2 多副本 + 客户端负载均衡方案
Spring AI 本身不直接提供负载均衡能力,但结合 MCP 的多连接配置和自定义逻辑,可以实现一套轻量级的方案:
架构设计:
+------------------+
| AI Application |
| (MCP Client) |
+--------+---------+
|
+--------+---------+
| Load Balancer |
| (自定义逻辑) |
+--------+---------+
|
+--------------+--------------+
| | |
+-----+----+ +-----+----+ +-----+----+
| KB Srv 1 | | KB Srv 2 | | KB Srv 3 |
+----------+ +----------+ +----------+
负载均衡策略:
- Round Robin:轮询分发
- Weighted:按权重分配
- Least Connections:最少连接优先
- Health Check:健康检查 + 自动剔除
方案一:配置多个 SSE 连接 + 自定义路由
yaml
spring:
ai:
mcp:
client:
sse:
connections:
kb-service-1:
url: https://kb-svc-1.internal:8083
kb-service-2:
url: https://kb-svc-2.internal:8083
kb-service-3:
url: https://kb-svc-3.internal:8083
然后通过 McpToolFilter 控制只暴露一组工具(避免重复注册):
java
@Component
public class LoadBalancedToolFilter implements McpToolFilter {
/**
* 多副本场景下,只暴露第一个连接的工具
* 其他连接作为故障转移备用
*/
@Override
public boolean test(McpConnectionInfo connectionInfo, McpSchema.Tool tool) {
String serverName = connectionInfo.serverName();
// kb-service-2 和 kb-service-3 的工具不直接暴露
// 但保持连接活跃(用于故障转移)
if (serverName.startsWith("kb-service-") && !serverName.equals("kb-service-1")) {
return false;
}
return true;
}
}
方案二:健康检查 + 动态切换
更成熟的方案是引入健康检查,在主节点不可用时自动切换到备用节点:
java
@Component
public class McpHealthChecker {
private final Map<String, Boolean> serverHealth = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
@Autowired
private List<McpSyncClient> mcpClients;
@PostConstruct
public void startHealthCheck() {
// 每 30 秒检查一次所有服务器健康状态
scheduler.scheduleAtFixedRate(this::checkAllServers, 10, 30, TimeUnit.SECONDS);
}
private void checkAllServers() {
for (McpSyncClient client : mcpClients) {
String serverName = client.getServerInfo().name();
try {
// 通过 ping 检查服务器是否可用
client.ping();
serverHealth.put(serverName, true);
log.debug("Server [{}] is healthy", serverName);
} catch (Exception e) {
serverHealth.put(serverName, false);
log.warn("Server [{}] is unhealthy: {}", serverName, e.getMessage());
}
}
}
public boolean isHealthy(String serverName) {
return serverHealth.getOrDefault(serverName, false);
}
public String getHealthyServer(String... candidates) {
// 优先返回第一个健康的服务器
for (String candidate : candidates) {
if (isHealthy(candidate)) {
return candidate;
}
}
throw new RuntimeException("No healthy MCP server available among: "
+ Arrays.toString(candidates));
}
}
方案三:通过 Gateway 统一入口
对于大规模部署,更推荐在 MCP Server 前面加一个网关层:
+------------------+ +------------------+ +------------------+
| AI Application |---->| API Gateway / |---->| KB Service |
| (MCP Client) | | Load Balancer | | Instance 1 |
+------------------+ | (Nginx/Envoy) | +------------------+
| | +------------------+
| SSE / HTTP LB |---->| KB Service |
| Health Check | | Instance 2 |
+------------------+ +------------------+
优势:
1. AI 应用只看到一个 MCP Server 地址
2. 网关负责健康检查、负载均衡
3. 可以独立扩缩容后端 MCP Server
网关配置示例(Nginx SSE 代理):
nginx
upstream mcp_kb_backend {
least_conn;
server kb-svc-1:8083 max_fails=3 fail_timeout=30s;
server kb-svc-2:8083 max_fails=3 fail_timeout=30s;
server kb-svc-3:8083 max_fails=3 fail_timeout=30s;
}
server {
listen 8083;
location /mcp {
proxy_pass http://mcp_kb_backend;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
# SSE 支持
proxy_set_header Accept "text/event-stream";
chunked_transfer_encoding off;
}
}
5.3 故障转移实战
结合健康检查,实现自动故障转移的核心逻辑:
java
@Service
public class McpServiceWithFailover {
private final McpHealthChecker healthChecker;
private final Map<String, McpSyncClient> clientMap;
/**
* 带故障转移的工具调用
*/
public McpSchema.CallToolResult callToolWithFailover(
String toolName,
Map<String, Object> arguments) {
// 按优先级排列的服务器列表
List<String> serverCandidates = List.of(
"kb-service-1",
"kb-service-2",
"kb-service-3"
);
Exception lastException = null;
for (String serverName : serverCandidates) {
McpSyncClient client = clientMap.get(serverName);
if (client == null || !healthChecker.isHealthy(serverName)) {
log.debug("Skipping unhealthy server: {}", serverName);
continue;
}
try {
McpSchema.CallToolResult result = client.callTool(
new McpSchema.CallToolRequest(toolName, arguments));
log.info("Tool [{}] called successfully on server [{}]",
toolName, serverName);
return result;
} catch (Exception e) {
lastException = e;
log.warn("Tool [{}] failed on server [{}]: {}",
toolName, serverName, e.getMessage());
healthChecker.markUnhealthy(serverName);
}
}
throw new McpServiceUnavailableException(
"All MCP servers failed for tool: " + toolName, lastException);
}
}
5.4 熔断与降级策略
对于关键的 MCP 工具调用,还需要考虑熔断和降级:
java
@Component
public class McpCircuitBreaker {
private final Map<String, CircuitState> circuits = new ConcurrentHashMap<>();
public enum CircuitState {
CLOSED, // 正常工作
OPEN, // 熔断打开,拒绝调用
HALF_OPEN // 半开,试探性放行
}
/**
* 熔断器包装的工具调用
*/
public <T> T executeWithCircuitBreaker(
String serverName,
Supplier<T> mcpCall,
Supplier<T> fallback) {
CircuitState state = circuits.computeIfAbsent(
serverName, k -> new CircuitState(CircuitState.CLOSED, 0, System.currentTimeMillis()));
if (state.state == CircuitState.OPEN) {
// 检查是否到了半开探查时间(30秒后)
if (System.currentTimeMillis() - state.lastFailureTime > 30_000) {
state.state = CircuitState.HALF_OPEN;
} else {
log.warn("Circuit OPEN for [{}], using fallback", serverName);
return fallback.get();
}
}
try {
T result = mcpCall.get();
// 调用成功,重置熔断器
state.state = CircuitState.CLOSED;
state.failureCount = 0;
return result;
} catch (Exception e) {
state.failureCount++;
state.lastFailureTime = System.currentTimeMillis();
// 连续失败 3 次则熔断
if (state.failureCount >= 3) {
state.state = CircuitState.OPEN;
log.error("Circuit OPEN for [{}] after {} failures",
serverName, state.failureCount);
}
return fallback.get();
}
}
record CircuitState(CircuitState state, int failureCount, long lastFailureTime) {}
}
六、实战:构建跨系统 AI 应用
6.1 项目结构
把前面讲的所有内容整合到一个完整的项目中:
intelligent-platform/
├── pom.xml
├── src/main/java/com/example/platform/
│ ├── PlatformApplication.java
│ ├── config/
│ │ ├── McpClientConfig.java
│ │ ├── McpServerConfig.java
│ │ └── ChatClientConfig.java
│ ├── mcp/
│ │ ├── tools/
│ │ │ └── PlatformMcpTools.java
│ │ ├── client/
│ │ │ └── McpClientHandlers.java
│ │ └── health/
│ │ ├── McpHealthChecker.java
│ │ └── McpCircuitBreaker.java
│ ├── service/
│ │ └── IntelligentAssistantService.java
│ └── controller/
│ └── AssistantController.java
└── src/main/resources/
└── application.yml
6.2 完整配置文件
yaml
server:
port: 8080
spring:
ai:
chat:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7
mcp:
client:
enabled: true
type: SYNC
request-timeout: 30s
toolcallback:
enabled: true
stdio:
connections:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/data/shared"]
sse:
connections:
order-service:
url: ${ORDER_MCP_URL:https://order-svc:8081}
sse-endpoint: /mcp/sse
crm-service:
url: ${CRM_MCP_URL:https://crm-svc:8082}
streamable-http:
connections:
knowledge-base:
url: ${KB_MCP_URL:https://kb-svc:8083}
endpoint: /mcp
server:
type: SYNC
stdio: false
server-info:
name: platform-mcp-server
version: 1.0.0
6.3 核心服务实现
java
@Service
@Slf4j
public class IntelligentAssistantService {
private final ChatClient chatClient;
private final McpHealthChecker healthChecker;
private final McpCircuitBreaker circuitBreaker;
public IntelligentAssistantService(
ChatClient chatClient,
McpHealthChecker healthChecker,
McpCircuitBreaker circuitBreaker) {
this.chatClient = chatClient;
this.healthChecker = healthChecker;
this.circuitBreaker = circuitBreaker;
}
/**
* 通用智能助手入口
*/
public String chat(String userMessage, String userId, String sessionId) {
ToolContext context = ToolContext.builder()
.put("userId", userId)
.put("sessionId", sessionId)
.put("timestamp", Instant.now().toString())
.build();
return chatClient.prompt()
.system("""
你是一个企业级智能助手,可以访问以下能力:
- 订单查询与管理
- 客户信息查询
- 知识库检索
- 文件系统操作
请根据用户的问题,选择最合适的工具来获取信息并回答。
回答时要准确、简洁,并标注信息来源。
""")
.user(userMessage)
.toolCallbacks()
.advisors(new ToolContext(context))
.call()
.content();
}
/**
* 专用场景:订单智能处理
*/
public String handleOrderQuery(String orderId, String customerId) {
// 使用熔断器保护 MCP 调用
return circuitBreaker.executeWithCircuitBreaker(
"order-service",
() -> chatClient.prompt()
.system("你是订单处理专家,请帮助客户处理订单相关问题。")
.user("客户 %s 需要了解订单 %s 的详细信息,并提供处理建议。"
.formatted(customerId, orderId))
.toolCallbacks()
.advisors(new ToolContext(Map.of("customerId", customerId)))
.call()
.content(),
// 降级:MCP 不可用时返回提示
() -> "抱歉,订单服务暂时不可用,请稍后重试或联系人工客服。"
);
}
}
6.4 REST 控制器
java
@RestController
@RequestMapping("/api/assistant")
@Slf4j
public class AssistantController {
private final IntelligentAssistantService assistantService;
public AssistantController(IntelligentAssistantService assistantService) {
this.assistantService = assistantService;
}
@PostMapping("/chat")
public ResponseEntity<ChatResponse> chat(
@RequestBody ChatRequest request,
@RequestHeader("X-User-Id") String userId) {
log.info("Chat request from user [{}]: {}", userId, request.message());
String answer = assistantService.chat(
request.message(), userId, UUID.randomUUID().toString());
return ResponseEntity.ok(new ChatResponse(answer, Instant.now()));
}
@PostMapping("/order/{orderId}")
public ResponseEntity<ChatResponse> handleOrder(
@PathVariable String orderId,
@RequestBody OrderQueryRequest request,
@RequestHeader("X-User-Id") String userId) {
String answer = assistantService.handleOrderQuery(orderId, userId);
return ResponseEntity.ok(new ChatResponse(answer, Instant.now()));
}
@GetMapping("/health/mcp")
public ResponseEntity<Map<String, Object>> mcpHealthStatus() {
Map<String, Object> status = Map.of(
"servers", healthChecker.getAllHealthStatus(),
"timestamp", Instant.now()
);
return ResponseEntity.ok(status);
}
record ChatRequest(String message) {}
record OrderQueryRequest(String customerId) {}
record ChatResponse(String answer, Instant timestamp) {}
}
写在最后
MCP 协议正在成为 AI 应用的"USB 接口"标准。Spring AI 通过深度集成 MCP SDK,让 Java 开发者可以用最熟悉的 Spring Boot 方式来构建标准化的 AI 工具链。
回顾一下这篇覆盖的核心内容:
-
MCP SDK 0.17.2 升级 :
McpSyncServerCustomizer/McpAsyncServerCustomizer提供了服务器构建阶段的扩展能力,非 Web 上下文自动配置修复让命令行和批处理场景也能使用 MCP。 -
MCP Client 配置:三种传输方式(Stdio/SSE/Streamable HTTP)覆盖了从本地进程到远程服务的所有场景,配合四个扩展点(Customizer、Filter、PrefixGenerator、MetaConverter)可以实现精细化的控制。
-
Server-Tool Calling 桥接 :通过
ToolCallbackProvider自动桥接,LLM 看到的是一个统一的工具列表,不关心工具来自哪个 MCP Server。 -
多服务器编排:Spring AI 天然支持多服务器连接,通过网关层可以实现真正的负载均衡。
-
故障转移:健康检查 + 熔断器 + 降级策略,构建生产级别的容错能力。
对于正在做 AI 应用落地的团队,我的建议是:先用 MCP 把内部能力标准化暴露出来,不用一步到位搞复杂的编排,先把工具层打通,后续再根据业务量级引入负载均衡和容错机制。
项目源码参考:spring-ai-examples MCP 模块