本文最后更新于 2025-07-21,文章内容可能已经过时。请谨慎检查,如果谬误,请评论联系

MCP Client

阅读

MCP Client Boot Starter :: Spring AI Reference

实践和原理分析

基本介绍

Spring AI MCP Client一共支持三种类型:

  • StdioClientTransport 标准输入输出
  • WebFluxSseClientTransport 基于 WebFluxSSE模式
  • HttpClientSseClientTransport 基于 HttpClintSSE模式

根据使用的依赖包或者Web类型可以支持不同的Transport。其中 StdioClientTransport 每种模式都支持。

实践发现一个Server加载两次的bug

SpringAI 1.0.0-M6版本WebFlux SSE的MCP Client 有自动注入缺陷,导致Server注入两次,报错

这个是描述的缺陷的 ISSUE: https://github.com/spring-projects/spring-ai/pull/2333

C39A0C8E-DC5F-4D97-9B17-DB8519831724.png整体加载逻辑是先加载 WebFluxSSeClientTransport,然后加载 HttpClientSseClientTransport 并且添加了条件判断。但是判断条件如图,写错了,可能是 copy类名的时候,copy多了。导致 SSE模式加载 MCP Server的时候,因为这个条件出错,会以 HttpClientWebFlux的方式各加载一遍。报错名称重复。

MCP Server 加载过程和使用原理

直接注册为工具的 beanToolCallbackProvider从这里看注入的是 McpSyncClient 这里根据同步或者异步,文档以同步为例

@Bean  
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",  
       matchIfMissing = true)  
public ToolCallbackProvider toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {  
    List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();  
    return new SyncMcpToolCallbackProvider(mcpClients);  
}

@Bean  
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",  
       matchIfMissing = true)  
public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientConfigurer,  
       McpClientCommonProperties commonProperties,  
       ObjectProvider<List<NamedClientMcpTransport>> transportsProvider) {  
  
    List<McpSyncClient> mcpSyncClients = new ArrayList<>();  
  
    List<NamedClientMcpTransport> namedTransports = transportsProvider.stream().flatMap(List::stream).toList();  
  
    if (!CollectionUtils.isEmpty(namedTransports)) {  
       for (NamedClientMcpTransport namedTransport : namedTransports) {  
  
          McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),  
                commonProperties.getVersion());  
  
          McpClient.SyncSpec syncSpec = McpClient.sync(namedTransport.transport())  
             .clientInfo(clientInfo)  
             .requestTimeout(commonProperties.getRequestTimeout());  
  
          syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec);  
  
          var syncClient = syncSpec.build();  
  
          if (commonProperties.isInitialized()) {  
             syncClient.initialize();  
          }  
  
          mcpSyncClients.add(syncClient);  
       }  
    }  
  
    return mcpSyncClients;  
}

接下来是 McpSyncClient 的加载,需要的是 ObjectProvider<List<NamedClientMcpTransport>> transportsProvider ,这个即是由上面三个 Transport提供的。

场景

  1. 使用STDIO的方式hack news
  2. Spring-ai example的 get weather server
    配置文件:
mcp:  
      client:  
        sse:  
          connections:  
            my-weather-server: http://localhost:8081  
#          spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080  
#          spring.ai.mcp.client.sse.connections.server2.url=http://localhost:8081  
        stdio:  
          servers-configuration: classpath:/mcp-servers-config.json

pom: 这里排除了webflux,否则一些阻塞方法会报错

<dependency>  
    <groupId>org.springframework.ai</groupId>  
    <artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>  
    <version>${spring-ai.version}</version>  
    <exclusions>  
        <exclusion>  
            <groupId>org.springframework.boot</groupId>  
            <artifactId>spring-boot-starter-webflux</artifactId>  
        </exclusion>  
    </exclusions>  
</dependency>

使用:

private final ToolCallbackProvider toolCallbackProvider;
...
public Flux<String> chatStreamWithToolsJdbcMemory(String chatId, String prompt) {  
    Message systemMessage = new SystemMessage(demoAiDefaultProperties.getSystemPrompt());  
    log.info("toolCallBack tools {}", Arrays.stream(toolCallbackProvider.getToolCallbacks()).map(FunctionCallback::getName).collect(Collectors.joining("\n")));  
    return chatClient.prompt(new Prompt(List.of(systemMessage, new UserMessage(prompt))))  
            .tools(hupuHostListToolsService)  
            .tools(toolCallbackProvider)  
            .advisors(new MessageChatMemoryAdvisor(chatMemory))  
            .advisors(advisorSpec -> advisorSpec  
                    .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)  
                    .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)  
            )            .stream().content();  
}