前言

MCP 是一个开放协议,它为应用程序向 LLM 提供上下文的方式进行了标准化。你可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化的方式一样,MCP 为 AI 模型连接各种数据源和工具提供了标准化的接口
image.png

Spring AI 不断的与时俱进,也支持了MCP协议,这使得我们这些普通程序员也能够快速搭建和引入MCP服务。

PixPin_2025-04-29_20-01-57.png

Spring AI 集成 MCP的几种方式

可以通过引入一些依赖,直接让 Spring AI 和 MCP 进行集成,在 Spring Boot 项目中轻松使用。下面是实现的几种方式:

(1)客户端启动器

  • spring-ai-starter-mcp-client:核心启动器,提供 STDIO 和基于 HTTP 的 SSE 支持

  • spring-ai-starter-mcp-client-webflux:基于 WebFlux 的 SSE 流式传输实现

(2)服务器启动器

  • spring-ai-starter-mcp-server:核心服务器,具有 STDIO 传输支持

  • spring-ai-starter-mcp-server-webmvc:基于 Spring MVC 的 SSE 流式传输实现

  • spring-ai-starter-mcp-server-webflux:基于 WebFlux 的 SSE 流式传输实现

本文所采用的是webflux,因为这种方式后续部署和维护最方便。

服务端

1. 引入依赖
<!--mcp服务器核心依赖— 响应式-->  
<dependency>  
    <groupId>org.springframework.ai</groupId>  
    <artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>  
    <version>1.0.0-M6</version>  
    </dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>  
    <artifactId>okhttp</artifactId>  
    <version>4.12.0</version>  
</dependency>  
2. 配置文件
server:  
  port: 8090  
spring:  
  application:  
    name: mcp-server  
  ai:  
    mcp:  
      server:  
        name: mcp-server # MCP服务器名称  
        version: 0.0.1   # 服务器版本号
3. 代码

McpServerApplication.java

@SpringBootApplication  
public class McpServerApplication {  
  
    public static void main(String[] args) {  
        SpringApplication.run(McpServerApplication.class, args);  
    }  
    @Bean  
    public ToolCallbackProvider serverTools(MCPService MCPService) {  
        return MethodToolCallbackProvider.builder().toolObjects(MCPService).build();  
    }  
  
}

MCPService.java

/**  
 * 使用Tavily进行AI搜索  
 * @param query 搜索关键词  
 * @param maxResults 最大返回结果数  
 * @return JSON格式的搜索结果(失败时返回错误信息)  
 */  
@Tool(name = "tavilySearch", description = "执行AI驱动的互联网搜索")  
public String tavilySearch(  
        @ToolParam(description = "搜索关键词", required = true) String query,  
        @ToolParam(description = "最大返回结果数") int maxResults  
) {  
    try {  
        TavilySearch tavilySearch = new TavilySearch();  
        List<Map<String, String>> results = tavilySearch.tavilySearch(query, maxResults);  
        log.debug("Tavily search completed for query: {}, results: {}", query, results.size());  
        return new ObjectMapper().writeValueAsString(results);  
    } catch (Exception e) {  
        log.error("Tavily search failed for query: {}", query, e);  
        return "{\"error\":\"Search failed: " + e.getMessage() + "\"}";  
    }  
}

TavilySearch.java

 // 搜索引擎  
    private String baseUrl = "https://api.tavily.com/search";  
    // apikey  
    private String apiKey = "tvly-XXX"; //Tavily的apikey需要替换为自己的
    private final OkHttpClient client;  
    private final ObjectMapper objectMapper;  
  
    public TavilySearch() {  
        this.client = new OkHttpClient.Builder()  
                .connectTimeout(30, TimeUnit.SECONDS)  
                .readTimeout(30, TimeUnit.SECONDS)  
                .build();  
        this.objectMapper = new ObjectMapper();  
    }  
  
    public List<Map<String, String>> tavilySearch(String query, int maxResults) {  
        List<Map<String, String>> results = new ArrayList<>();  
        try {  
            Map<String,Object> requestBody = new HashMap<>();  
            requestBody.put("query", query);  
//            requestBody.put("search_depth", searchDepth);  
            requestBody.put("max_results", maxResults);  
//            requestBody.put("time_range", timeRange);  
  
            Request request = new Request.Builder()  
                    .url(baseUrl)  
                    .post(RequestBody.create(MediaType.parse("application/json"), objectMapper.writeValueAsString(requestBody)))  
                    .header("Content-Type", "application/json")  
                    .header("Authorization", "Bearer" + apiKey)  
                    .build();  
  
  
            try (Response response = client.newCall(request).execute()) {  
                if (!response.isSuccessful()) throw new IOException("请求失败: " + response);  
  
                JsonNode jsonNode = objectMapper.readTree(response.body().string()).get("results");  
  
                if (!jsonNode.isEmpty()) {  
                    jsonNode.forEach(data -> {  
                        Map<String, String> processedResult = new HashMap<>();  
                        processedResult.put("title", data.get("title").toString());  
                        processedResult.put("url", data.get("url").toString());  
                        processedResult.put("content", data.get("content").toString());  
                        results.add(processedResult);  
                    });  
                }  
            }  
        } catch (Exception e) {  
            System.err.println("搜索时发生错误: " + e.getMessage());  
        }  
        return results;  
    }

这里Tavily的apikey需要替换为自己的,申请地址:
https://tavily.com/

客户端

1. 引入依赖
<dependency>  
    <groupId>org.springframework.ai</groupId>  
    <artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>  
    <version>1.0.0-M6</version>  
</dependency>
2. 配置文件
spring:
	ai:  
	  mcp:  
	    client:  
	      enabled: true  
	      name: mcp-client  
	      version: 1.0.0  
	      request-timeout: 120s  
	      type: ASYNC # 类型同步或者异步  
	      sse:  
	        connections:  
	          server1:  
	            url: http://localhost:8090
3. 代码
@RestController  
@RequestMapping("/mcp")  
@Tag(name = "MCP")  
@Slf4j  
public class MCPController {  
  
    @Autowired  
    private ChatClient chatClient;  
  
    @PostMapping(value = "/client", produces = MediaType.TEXT_EVENT_STREAM_VALUE)  
    public Flux<String> client(String message) {  
        log.info("正在连接MCP服务器: {}", mcpServerUrl);  
        var transport = new WebFluxSseClientTransport(WebClient.builder().baseUrl(mcpServerUrl));  
  
        return Flux.using(  
                () -> McpClient.sync(transport).build(),  
                client -> {  
                    client.initialize();  
                    try {  
                        List<McpSchema.Tool> tools = client.listTools().tools();  
                        String name = "tavilySearch";  
                        log.info("Available Tools = {}", tools);  
  
                        McpSchema.CallToolResult mcpResult = null;  
                        //无参数调用mcp  
//                        mcpResult = client.callTool(new McpSchema.CallToolRequest(name, Map.of()));  
                        //有参数调用mcp  
                        mcpResult = client.callTool(new McpSchema.CallToolRequest(name, Map.of("query", message, "maxResults", 5)));  
  
                        String content = mcpResult.content().toString();  
  
                        String mcpResultString = "{" + name + " : " + content + "}, ";  
                        log.info("最终请求信息{}", mcpResultString + message);  
                        // 先返回MCP的结果,然后再返回chatClient的流式响应  
                        return Flux.concat(  
                                Flux.just(mcpResultString),  
                                chatClient.prompt()  
                                        .user(mcpResultString + message)  
                                        .stream()  
                                        .content()  
                        );  
                    } catch (Exception e) {  
                        log.error("MCP调用出错: ", e);  
                        return Flux.error(new RuntimeException("MCP调用出错: " + e.getMessage()));  
                    }  
                }  
        );  
    }  
}

测试

由结果图可以看到大模型回答前都会先调用mcp的联网搜索然后再回答,这样就解决大模型语料库落后的问题了。
在这里插入图片描述
下面是实际应用的项目:
https://yiyongai.cn/

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐