Spring AI 2.0 MCP 协议实战:Model Context Protocol SDK 与多服务器编排

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 ,同时引入了 McpSyncServerCustomizerMcpAsyncServerCustomizer 等关键接口,修复了非 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 需要同时访问:

  1. 订单系统(Java 微服务)------ 查询订单状态
  2. 商品知识库(Elasticsearch)------ 检索商品信息
  3. 退换货流程引擎(Python 服务)------ 执行退换货操作
  4. 内部文档库(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,这个版本的升级包含几个关键变化:

新增 McpSyncServerCustomizerMcpAsyncServerCustomizer 接口

之前版本中,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 工具链。

回顾一下这篇覆盖的核心内容:

  1. MCP SDK 0.17.2 升级McpSyncServerCustomizer / McpAsyncServerCustomizer 提供了服务器构建阶段的扩展能力,非 Web 上下文自动配置修复让命令行和批处理场景也能使用 MCP。

  2. MCP Client 配置:三种传输方式(Stdio/SSE/Streamable HTTP)覆盖了从本地进程到远程服务的所有场景,配合四个扩展点(Customizer、Filter、PrefixGenerator、MetaConverter)可以实现精细化的控制。

  3. Server-Tool Calling 桥接 :通过 ToolCallbackProvider 自动桥接,LLM 看到的是一个统一的工具列表,不关心工具来自哪个 MCP Server。

  4. 多服务器编排:Spring AI 天然支持多服务器连接,通过网关层可以实现真正的负载均衡。

  5. 故障转移:健康检查 + 熔断器 + 降级策略,构建生产级别的容错能力。

对于正在做 AI 应用落地的团队,我的建议是:先用 MCP 把内部能力标准化暴露出来,不用一步到位搞复杂的编排,先把工具层打通,后续再根据业务量级引入负载均衡和容错机制。

项目源码参考:spring-ai-examples MCP 模块

相关推荐
大师影视解说2 小时前
2026 短剧出海:百亿市场洗牌,自动化翻译与工程本地化
人工智能·视频技术·短剧出海·短剧行业趋势·短剧翻译·视频翻译技术·行业观察
哈哈很哈哈2 小时前
深度学习中的分布式并行策略和内存优化技术
人工智能·语言模型
摩尔元数2 小时前
2026年PLC控制器工厂选MES,厂商推荐
人工智能·低代码·制造·mes
Web3VentureView2 小时前
倒计时 12 小时,SYNBO 主网即将上线!
大数据·人工智能·金融·web3·区块链
逸尘谈PM2 小时前
智能体框架对比:OpenClaw、LangChain、AutoGPT、CrewAI 深度对比
人工智能·ai·langchain·职场·2026年
AEIC学术交流中心2 小时前
【快速EI检索 | ACM出版】第三届机器学习与智能计算国际学术会议(MLIC 2026)
人工智能·机器学习
nap-joker2 小时前
【综述型论文+知识增强深度学习KADL】知识增强深度学习及其应用:一项综述
人工智能·深度学习·知识增强深度学习·kadl·经验知识·科学知识·知识识别
l软件定制开发工作室2 小时前
Spring开发系列教程(34)——打包Spring Boot应用
java·spring boot·后端·spring·springboot
0xDevNull2 小时前
Spring Boot 循环依赖解决方案完全指南
java·开发语言·spring