微信扫码
添加专属顾问
 
                        我要投稿
Java开发者如何高效集成MCP服务器,打破Python开发局限。 核心内容: 1. MCP Java开发背景与必要性 2. MCP协议概述及其核心特性 3. Java集成MCP的架构与传输实现
 
                                背景
目前主流的MCP server的开发语言是python,而在实际业务开发场景中,很多业务的后端开发语言都是Java,如果想在业务中集成各种开箱即用的MCP server,需要打通两种语言之间的壁垒。经过前期调研,发现目前网络中有关Java与MCP结合的资料很少,MCP双端使用不同语言进行开发的更是没有,因此花了一周的时间,通过阅读MCP官方文档、源码,调研目前目前主流的集成MCP的Java开发框架Spring AI,深度探索了Java开发者使用MCP的一些道路,仅供参考。
注意⚠️:Java开发MCP需要的JDK版本至少为17,springboot版本至少为3.0.0。
MCP概述
什么是MCP?
MCP(Model Context Protocol,模型上下文协议)是一种标准化的通信协议,旨在连接 AI 模型与工具链,提供统一的接口以支持动态工具调用、资源管理、对话状态同步等功能。它允许开发者构建灵活的 AI 应用程序,与不同的模型和工具进行交互,同时保持协议的可扩展性和跨语言兼容性。
特性
mcp模块中,不需要外部 Web 框架):核心io.modelcontextprotocol.sdk:mcp模块提供默认的 STDIO 和 SSE 客户端和服务器传输实现,而无需外部 Web 框架。
为了方便使用 Spring [7]框架,Spring 特定的传输可作为可选依赖项使用。
架构
SDK 遵循分层架构,关注点清晰分离:
MCP 客户端是模型上下文协议 (MCP) 架构中的关键组件,负责建立和管理与 MCP 服务器的连接。它实现了协议的客户端功能。
MCP 服务器是模型上下文协议 (MCP) 架构中的基础组件,为客户端提供工具、资源和功能。它实现了协议的服务器端。
主要作用:
依赖项
核心 MCP 功能:
<dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId></dependency>
核心mcp模块已经包含默认的 STDIO 和 SSE 传输实现,并且不需要外部 Web 框架。
如果使用 Spring 框架并希望使用 Spring 特定的传输实现,添加以下可选依赖项之一:
<!-- Optional: Spring WebFlux-based SSE client and server transport --><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp-spring-webflux</artifactId></dependency><!-- Optional: Spring WebMVC-based SSE server transport --><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp-spring-webmvc</artifactId></dependency>
物料清单 (BOM) 声明了特定版本使用的所有依赖项的推荐版本。使用应用构建脚本中的 BOM 可以避免自行指定和维护依赖项版本。相反,使用的 BOM 版本决定了所使用的依赖项版本。这还能确保默认使用受支持且经过测试的依赖项版本,除非选择覆盖这些版本。
将 BOM 添加到项目中:
<dependencyManagement> <dependencies> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-bom</artifactId> <version>0.9.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies></dependencyManagement>
将版本号替换为要使用的 BOM 版本。
以下依赖项可用并由 BOM 管理:
io.modelcontextprotocol.sdk:mcp- 核心 MCP 库提供模型上下文协议 (MCP) 实现的基本功能和 API,包括默认的 STDIO 和 SSE 客户端及服务器传输实现。无需任何外部 Web 框架。io.modelcontextprotocol.sdk:mcp-spring-webflux- 基于 WebFlux 的服务器发送事件 (SSE) 传输实现,适用于反应式应用程序。io.modelcontextprotocol.sdk:mcp-spring-webmvc- 基于 WebMVC 的服务器发送事件 (SSE) 传输实现,适用于基于 servlet 的应用程序。io.modelcontextprotocol.sdk:mcp-test- 测试实用程序并支持基于 MCP 的应用程序。Java->Java(MCP Java SDK)
MCP官方提供了模型上下文协议的 Java 实现供Java开发者使用,支持通过同步和异步通信模式与 AI 模型和工具进行标准化交互。官方文档里有详细的server和client开发的指南,大家可自行前往查看学习,不再赘述。
MCP原生Java SDK:https://github.com/modelcontextprotocol/java-sdk
MCP 客户端:https://modelcontextprotocol.io/sdk/java/mcp-client
MCP 服务端:https://modelcontextprotocol.io/sdk/java/mcp-server
当前绝大部分Java开发者都在使用Spring作为后端开发框架,因此接下来将着重介绍Spring AI中如何集成MCP能力。
Java->Java(Spring AI)
Spring AI MCP通过 Spring Boot 集成扩展了 MCP Java SDK,提供客户端[8]和服务器启动器[9]。
Spring AI MCP文档:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html
SpringBoot集成MCP
Spring AI 通过以下 Spring Boot 启动器提供 MCP 集成:
spring-ai-starter-mcp-client- 核心启动器提供 STDIO 和基于 HTTP 的 SSE 支持;spring-ai-starter-mcp-client-webflux- 基于 WebFlux 的 SSE 传输实现;spring-ai-starter-mcp-server- 具有 STDIO 传输支持的核心服务器;spring-ai-starter-mcp-server-webmvc- 基于Spring MVC的SSE传输实现;spring-ai-starter-mcp-server-webflux- 基于 WebFlux 的 SSE 传输实现;spring-ai-starter-mcp太过黑盒,中间的client创建连接等等过程都包装在源码中,且无法自定义,因此只适用于client和server端都是用spring AI开发的mcp应用。
SSE VS STDIO
在开发之前,我们需要先了解在MCP通信协议中,一般有两种模式,分别为 SSE(Server-Sent Events)和STDIO(标准输入/输出) 。
Accept: text/event-stream 的 GET 请求,与服务器建立一个持久化的连接,服务器可以随时把事件流(文本格式)推送到客户端,客户端通过 JavaScript 的 EventSource API 监听并处理。|)、重定向(>/<)等。grep、sed、ffmpeg 等通过管道串联,快速处理文本或二进制流;SSE
Service类:
package com.alibaba.damo.mcpserver.service;import org.springframework.ai.tool.annotation.Tool;import org.springframework.ai.tool.annotation.ToolParam;import org.springframework.stereotype.Service;import org.springframework.web.reactive.function.client.WebClient;/*** @author clong*/publicclassOpenMeteoService {privatefinal WebClient webClient;publicOpenMeteoService(WebClient.Builder webClientBuilder){this.webClient = webClientBuilder.baseUrl("https://api.open-meteo.com/v1").build();}(description = "根据经纬度获取天气预报")public String getWeatherForecastByLocation((description = "纬度,例如:39.9042") String latitude,(description = "经度,例如:116.4074") String longitude) {try {String response = webClient.get().uri(uriBuilder -> uriBuilder.path("/forecast").queryParam("latitude", latitude).queryParam("longitude", longitude).queryParam("current", "temperature_2m,wind_speed_10m").queryParam("timezone", "auto").build()).retrieve().bodyToMono(String.class).block();// 解析响应并返回格式化的天气信息return"当前位置(纬度:" + latitude + ",经度:" + longitude + ")的天气信息:\n" + response;} catch (Exception e) {return"获取天气信息失败:" + e.getMessage();}}(description = "根据经纬度获取空气质量信息")public String getAirQuality((description = "纬度,例如:39.9042") String latitude,(description = "经度,例如:116.4074") String longitude) {// 模拟数据,实际应用中应调用真实APIreturn"当前位置(纬度:" + latitude + ",经度:" + longitude + ")的空气质量:\n" +"- PM2.5: 15 μg/m³ (优)\n" +"- PM10: 28 μg/m³ (良)\n" +"- 空气质量指数(AQI): 42 (优)\n" +"- 主要污染物: 无";}}
启动类:
package com.alibaba.damo.mcpserver;import com.alibaba.damo.mcpserver.service.OpenMeteoService;import org.springframework.ai.tool.ToolCallbackProvider;import org.springframework.ai.tool.method.MethodToolCallbackProvider;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;import org.springframework.web.reactive.function.client.WebClient;publicclassMcpServerApplication {publicstaticvoidmain(String[] args){SpringApplication.run(McpServerApplication.class, args);}public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService){return MethodToolCallbackProvider.builder().toolObjects(openMeteoService).build();}public WebClient.Builder webClientBuilder(){return WebClient.builder();}}
配置文件:
server.port=8080spring.ai.mcp.server.name=my-weather-serverspring.ai.mcp.server.version=0.0.1
依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.alibaba.damo</groupId><artifactId>mcp-server</artifactId><version>0.0.1-SNAPSHOT</version><name>mcp-server</name><description>mcp-server</description><properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>3.0.2</spring-boot.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-server-webflux</artifactId><version>1.0.0-M7</version></dependency><dependency><groupId>io.netty</groupId><artifactId>netty-resolver-dns-native-macos</artifactId><version>4.1.79.Final</version><scope>runtime</scope><classifier>osx-aarch_64</classifier></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.damo.mcpserver.McpServerApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
启动日志:
2025-04-29T20:09:07.153+08:00 INFO 51478 --- [ main] c.a.damo.mcpserver.McpServerApplication : Starting McpServerApplication using Java 17.0.15 with PID 51478 (/Users/clong/IdeaProjects/mcp-server/target/classes started by clong in /Users/clong/IdeaProjects/mcp-server)2025-04-29T20:09:07.154+08:00 INFO 51478 --- [ main] c.a.damo.mcpserver.McpServerApplication : No active profile set, falling back to 1default profile: "default"2025-04-29T20:09:07.561+08:00 INFO 51478 --- [ main] o.s.a.m.s.a.McpServerAutoConfiguration : Registered tools: 2, notification: true2025-04-29T20:09:07.609+08:00 INFO 51478 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 80802025-04-29T20:09:07.612+08:00 INFO 51478 --- [ main] c.a.damo.mcpserver.McpServerApplication : Started McpServerApplication in 0.572 seconds (process running for0.765)2025-04-29T20:10:18.812+08:00 INFO 51478 --- [ctor-http-nio-3] i.m.server.McpAsyncServer : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=null, sampling=null], Info: Implementation[name=spring-ai-mcp-client - server1, version=1.0.0]
客户端只需要在启动类中构建ChatClient并注入MCP工具即可。
package com.alibaba.damo.mcpclient;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.tool.ToolCallbackProvider;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.context.annotation.Bean;import java.util.Arrays;publicclassMcpClientApplication {publicstaticvoidmain(String[] args){SpringApplication.run(McpClientApplication.class, args);}public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder,ToolCallbackProvider tools,ConfigurableApplicationContext context) {return args -> {// 构建ChatClient并注入MCP工具var chatClient = chatClientBuilder.defaultTools(tools).build();// 定义用户输入String userInput = "杭州今天天气如何?";// 打印问题System.out.println("\n>>> QUESTION: " + userInput);// 调用LLM并打印响应System.out.println("\n>>> ASSISTANT: " +chatClient.prompt(userInput).call().content());// 关闭应用上下文context.close();};}}
阿里云百炼平台提供各大模型百万token免费体验,可以直接去平台申请即可获取对应的sk。
https://bailian.console.aliyun.com/console?tab=api#/api
spring.ai.openai.api-key=sk-xxxspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1spring.ai.openai.chat.options.model=qwen-maxspring.ai.mcp.client.toolcallback.enabled=truespring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080spring.main.web-application-type=none
依赖中需要添加spring-ai-starter-mcp-client依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.alibaba.damo</groupId><artifactId>mcp-client</artifactId><version>0.0.1-SNAPSHOT</version><name>mcp-client</name><description>mcp-client</description><properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>3.4.0</spring-boot.version><mcp.version>0.9.0</mcp.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client</artifactId><version>1.0.0-M7</version></dependency><!-- openai model --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId><version>1.0.0-M7</version></dependency><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp</artifactId><version>0.9.0</version></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0-M7</version><type>pom</type><scope>import</scope></dependency><!-- MCP BOM 统一版本 --><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp-bom</artifactId><version>${mcp.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.damo.mcpclient.McpClientApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
客户端调用日志如下:
2025-04-29T20:10:18.302+08:00 INFO 51843 --- [ main] c.a.damo.mcpclient.McpClientApplication : Starting McpClientApplication using Java 17.0.15 with PID 51843 (/Users/clong/IdeaProjects/mcp-client/target/classes started by clong in /Users/clong/IdeaProjects/mcp-client)2025-04-29T20:10:18.303+08:00 INFO 51843 --- [ main] c.a.damo.mcpclient.McpClientApplication : No active profile set, falling back to 1default profile: "default"2025-04-29T20:10:18.846+08:00 INFO 51843 --- [ient-1-Worker-0] i.m.client.McpAsyncClient : Server response with Protocol: 2024-11-05, Capabilities: ServerCapabilities[experimental=null, logging=LoggingCapabilities[], prompts=null, resources=null, tools=ToolCapabilities[listChanged=true]], Info: Implementation[name=my-weather-server, version=0.0.1] and Instructions null2025-04-29T20:10:19.018+08:00 INFO 51843 --- [ main] c.a.damo.mcpclient.McpClientApplication : Started McpClientApplication in 0.842 seconds (process running for1.083)>>> QUESTION: 杭州今天天气如何?>>> ASSISTANT: 杭州当前的天气信息如下:- 温度:24.4°C- 风速:3.4 km/h请注意,这些信息是基于当前时间的实时数据。
STDIO
与SSE模式相比,服务端只需要修改配置文件即可。由于是通过标准输入输出的方式提供服务,服务端不需要开放端口,因此注释掉端口号。同时需要修改web应用类型为none,禁掉banner输出(原因后面会讲)。配置MCP server的类型为stdio,服务名称和版本号,以供客户端发现。
#server.port=8080spring.main.web-application-type=nonespring.main.banner-mode=offspring.ai.mcp.server.stdio=truespring.ai.mcp.server.name=my-weather-serverspring.ai.mcp.server.version=0.0.1
修改完之后通过maven package打包成jar文件。
客户端增加mcp-servers-config.json配置路径,启用toolcallback,注释掉sse连接。
spring.ai.openai.api-key=sk-XXXXXXspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1spring.ai.openai.chat.options.model=qwen-maxspring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.jsonspring.ai.mcp.client.toolcallback.enabled=true#spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080spring.main.web-application-type=none
MCP服务启动配置,这里的jar包为刚刚上面打包的服务端jar包。
{  "mcpServers": {    "weather": {      "command": "java",      "args": [        "-Dlogging.pattern.console=",        "-jar",        "/Users/clong/IdeaProjects/mcp-server/target/mcp-server-0.0.1-SNAPSHOT.jar"      ]    }  }}<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.alibaba.damo</groupId><artifactId>mcp-client</artifactId><version>0.0.1-SNAPSHOT</version><name>mcp-client</name><description>mcp-client</description><properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>3.4.0</spring-boot.version><mcp.version>0.9.0</mcp.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-mcp-client</artifactId><version>1.0.0-M7</version></dependency><!-- openai model --><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-openai</artifactId><version>1.0.0-M7</version></dependency><!-- <dependency>--><!-- <groupId>io.modelcontextprotocol.sdk</groupId>--><!-- <artifactId>mcp</artifactId>--><!-- <version>0.9.0</version>--><!-- </dependency>--></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0-M7</version><type>pom</type><scope>import</scope></dependency><!-- MCP BOM 统一版本 --><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp-bom</artifactId><version>${mcp.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.damo.mcpclient.McpClientApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
日志同上,不再打印。
重要配置项解析
Spring Boot 在启动时会根据类路径自动检测应用类型(WebApplicationType),并加载对应的自动配置:
ReactiveWebApplicationContext,并尝试注册 ReactiveWebServerFactory;
ServletWebServerApplicationContext,并尝试注册 ServletWebServerFactory;NONE,则不会初始化任何内嵌 Web 容器。spring-boot-starter-webflux,则不会创建 ReactiveWebServerFactory,导致启动 ReactiveWebApplicationContext 时抛出缺失 Bean 异常;spring-boot-starter-web,则不会创建 ServletWebServerFactory,会在启动 ServletWebServerApplicationContext 时抛出类似错误。当同时引入了 spring-web(Servlet)和 spring-webflux(Reactive)依赖时,Spring Boot 默认优先选择 Servlet 模式;若业务需要 Reactive,可显式设置 spring.main.web-application-type=reactive,否则仍然会走 Servlet 自动配置路径。
因此我们需要将该配置项设置为none,避免WebFlux或者Servlet容器报找不到错误。
MCP 客户端通过 STDIO 读取 JSON-RPC 消息时,会将 Spring Boot 的启动 Banner(ASCII 艺术 Logo)或其他非 JSON 文本内容当作输入交给 Jackson 解析,导致 MismatchedInputException: No content to map due to end-of-input 异常。为彻底避免非 JSON 文本污染标准输出流,需要在 Spring Boot 应用中禁用 Banner 输出,即在 application.properties 中配置 spring.main.banner-mode=off,或在代码中通过 SpringApplication 设置 Banner.Mode.OFF。
spring.ai.mcp.client.toolcallback.enabled 用于显式开启 Spring AI 与 Model Context Protocol (MCP) 之间的工具回调(ToolCallback)集成;该功能默认关闭,必须显式设置为 true 才会激活相应的自动配置并注册 ToolCallbackProvider Bean,以便在 ChatClient 中注入并使用 MCP 工具。
Java->Python
在实际开发过程中,对于上述两种模式,STDIO更加倾向于demo,对于企业级应用及大规模部署,采用SSE远程通信的方式可扩展性更强,且更加灵活,实现服务端与客户端的完全解耦。因此接下来我们默认采用SSE的模式来构建MCP通信。目前市面上绝大部分的MCP server代码都是用python开发的(AI时代加速了python的发展),对于Java开发者来说,我们想要实现最好不修改一行代码,无缝对接这些服务。
Model Context Protocol(MCP)基于 JSON-RPC 2.0,完全与语言无关,支持通过标准化的消息格式在任意编程语言间互通。因此,Java 实现的 MCP 客户端可以无缝地与Python 实现的 MCP 服务器通信,只要双方遵循相同的协议规范和传输方式即可。
1. MCP 的语言无关性
MCP 的底层通信协议是 JSON-RPC 2.0,它使用纯文本的 JSON 作为编码格式,极大地保证了跨语言互操作性。任何能读写 JSON 并通过 TCP/STDIO/HTTP/WebSocket 等传输层发送、接收文本的语言,都能实现对 MCP 消息的编解码。
Anthropic 和社区已经提供了多语言的 MCP SDK,包括 Python、Java、TypeScript、Kotlin、C# 等。各 SDK 都会对 JSON-RPC 消息进行封装,使得开发者只需调用相应方法即可,而无需关心底层细节。
2. 常见传输方式
MCP 消息既可通过标准输入/输出(STDIO)传输,也可通过HTTP(S) 或 WebSocket 进行通信。只要双方选用一致的传输通道,Java 客户端和 Python 服务器就能正常交换 JSON-RPC 消息。
3. Java侧实现方式
为了方便起见,这里的MCP服务端使用blender-mcp作为SSE服务端。
如果使用Spring AI开发的MCP客户端连接python开发的MCP服务端请求会报错。
python端报错:
2025-04-30T17:25:09.843+08:00 ERROR 69857 --- [onPool-worker-1] i.m.c.t.HttpClientSseClientTransport : Error sending message: 500
Java端报错:
INFO: 127.0.0.1:55085 - "GET /sse HTTP/1.1"200 OKWARNING: Unsupported upgrade request.INFO: 127.0.0.1:55087 - "POST /messages/?session_id=5b92a6377fcb4b3fa9f051b43d0379b5 HTTP/1.1"500 Internal Server ErrorERROR: Exception in ASGI applicationTraceback(most recent call last):File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 409, in run_asgiresult = await app( # type: ignore[func-returns-value]^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^self.scope, self.receive, self.send^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^)^File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__return await self.app(scope, receive, send)^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/applications.py", line 112, in __call__await self.middleware_stack(scope, receive, send)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 187, in __call__raise excFile "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 165, in __call__await self.app(scope, receive, _send)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 62, in __call__await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 53, in wrapped_appraise excFile "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_appawait app(scope, receive, sender)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 714, in __call__await self.middleware_stack(scope, receive, send)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 734, in appawait route.handle(scope, receive, send)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 460, in handleawait self.app(scope, receive, send)File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/mcp/server/sse.py", line 159, in handle_post_messagejson = await request.json()^^^^^^^^^^^^^^^^^^^^File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/requests.py", line 248, in jsonself._json = json.loads(body)~~~~~~~~~~^^^^^^File "/Users/clong/.local/share/uv/python/cpython-3.13.2-macos-aarch64-none/lib/python3.13/json/__init__.py", line 346, in loadsreturn _default_decoder.decode(s)~~~~~~~~~~~~~~~~~~~~~~~^^^File "/Users/clong/.local/share/uv/python/cpython-3.13.2-macos-aarch64-none/lib/python3.13/json/decoder.py", line 345, in decodeobj, end = self.raw_decode(s, idx=_w(s, 0).end())~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^File "/Users/clong/.local/share/uv/python/cpython-3.13.2-macos-aarch64-none/lib/python3.13/json/decoder.py", line 363, in raw_decoderaise JSONDecodeError("Expecting value", s, err.value) from Nonejson.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char0)
追源码发现Java MCP client在初始化阶段发送POST请求(见第二段代码的28行)body为空,导致python MCP server端json反序列化(第一段代码27行)空字符串b''失败。暂时无法解决。如果想要用Spring AI,就需要重写mcp server。
async def handle_post_message(self, scope: Scope, receive: Receive, send: Send) -> None:logger.debug("Handling POST message")request = Request(scope, receive)session_id_param = request.query_params.get("session_id")if session_id_param is None:logger.warning("Received request without session_id")response = Response("session_id is required", status_code=400)return await response(scope, receive, send)try:session_id = UUID(hex=session_id_param)logger.debug(f"Parsed session ID: {session_id}")except ValueError:logger.warning(f"Received invalid session ID: {session_id_param}")response = Response("Invalid session ID", status_code=400)return await response(scope, receive, send)writer = self._read_stream_writers.get(session_id)ifnot writer:logger.warning(f"Could not find session for ID: {session_id}")response = Response("Could not find session", status_code=404)return await response(scope, receive, send)json = await request.json()logger.debug(f"Received JSON: {json}")try:message = types.JSONRPCMessage.model_validate(json)logger.debug(f"Validated client message: {message}")except ValidationError as err:logger.error(f"Failed to parse message: {err}")response = Response("Could not parse message", status_code=400)await response(scope, receive, send)await writer.send(err)returnlogger.debug(f"Sending message to writer: {message}")response = Response("Accepted", status_code=202)await response(scope, receive, send)await writer.send(message)
public Mono<Void> sendMessage(JSONRPCMessage message){if (isClosing) {return Mono.empty();}try {if (!closeLatch.await(10, TimeUnit.SECONDS)) {return Mono.error(new McpError("Failed to wait for the message endpoint"));}}catch (InterruptedException e) {return Mono.error(new McpError("Failed to wait for the message endpoint"));}String endpoint = messageEndpoint.get();if (endpoint == null) {return Mono.error(new McpError("No message endpoint available"));}try {String jsonText = this.objectMapper.writeValueAsString(message);HttpRequest request = this.requestBuilder.uri(URI.create(this.baseUri + endpoint)).POST(HttpRequest.BodyPublishers.ofString(jsonText)).build();return Mono.fromFuture(httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()).thenAccept(response -> {if (response.statusCode() != 200 && response.statusCode() != 201 && response.statusCode() != 202&& response.statusCode() != 206) {logger.error("Error sending message: {}", response.statusCode());}}));}catch (IOException e) {if (!isClosing) {return Mono.error(new RuntimeException("Failed to serialize message", e));}return Mono.empty();}}
重写server的方式比较繁琐且不适用,对于目前绝大部分的MCP server都是python开发的现状下,尽量不动mcp server端的代码最好,因此,我尝试从client端着手,抛弃spring AI的包装,尝试使用原生的mcp java sdk+openai java sdk来实现一个类似Claude desktop这样的支持MCP调用的client。
3.2 mcp java sdk
package com.alibaba.damo.mcpclient.client;import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.core.JsonValue;import com.openai.models.FunctionDefinition;import com.openai.models.FunctionParameters;import com.openai.models.chat.completions.*;import io.modelcontextprotocol.client.McpClient;import io.modelcontextprotocol.client.McpSyncClient;import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;import io.modelcontextprotocol.spec.McpClientTransport;import io.modelcontextprotocol.spec.McpSchema;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.stream.Collectors;importstatic com.openai.models.chat.completions.ChatCompletion.Choice.FinishReason.TOOL_CALLS;/*** @author clong*/@ServicepublicclassMyMCPClient {privatestaticfinal Logger logger = LoggerFactory.getLogger(MyMCPClient.class);@Value("${spring.ai.openai.base-url}")private String baseUrl;@Value("${spring.ai.openai.api-key}")private String apiKey;@Value("${spring.ai.openai.chat.options.model}")private String model;// Tool 名称到 MCP Client 的映射privatefinal Map<String, McpSyncClient> toolToClient = new HashMap<>();@Value("${mcp.servers}") // e.g. tool1=http://url1,tool2=http://url2,...private String toolServerMapping;private OpenAIClient openaiClient;privatefinal List<McpSchema.Tool> allTools = new ArrayList<>();@PostConstructpublicvoidinit(){// 解析配置并初始化各 MCP ClientArrays.stream(toolServerMapping.split(",")).map(entry -> entry.split("=")).forEach(pair -> {String url = pair[1];McpClientTransport transport =HttpClientSseClientTransport.builder(url).build();McpSyncClient client = McpClient.sync(transport).build();client.initialize(); // 建立 SSE 连接logger.info("Connected to MCP server via SSE at {}", url);// 列出并打印所有可用工具List<McpSchema.Tool> tools = client.listTools().tools();logger.info("Available MCP tools:");tools.forEach(t -> logger.info(" - {} : {}", t.name(), t.description()));allTools.addAll(tools);tools.forEach(t -> toolToClient.put(t.name(), client));});// 2. 初始化 OpenAI 客户端this.openaiClient = OpenAIOkHttpClient.builder().baseUrl(baseUrl).apiKey(apiKey).checkJacksonVersionCompatibility(false).build();logger.info("OpenAI client initialized with model {}", model);}@PreDestroypublicvoidshutdown(){// 如果有必要,优雅关闭 MCP 客户端toolToClient.values().forEach((client) -> {try {client.close();logger.info("Closed MCP client for {}", client);} catch (Exception e) {logger.warn("Error closing MCP client for {}: {}", client, e.getMessage());}});}/*** 处理一次用户查询:注入所有工具定义 -> 发首轮请求 -> 若触发 function_call 则执行 ->* 再次发请求获取最终回复*/public String processQuery(String query){try {List<ChatCompletionTool> chatTools = allTools.stream().map(t -> ChatCompletionTool.builder().function(FunctionDefinition.builder().name(t.name()).description(t.description()).parameters(FunctionParameters.builder().putAdditionalProperty("type", JsonValue.from(t.inputSchema().type())).putAdditionalProperty("properties", JsonValue.from(t.inputSchema().properties())).putAdditionalProperty("required", JsonValue.from(t.inputSchema().required())).putAdditionalProperty("additionalProperties", JsonValue.from(t.inputSchema().additionalProperties())).build()).build()).build()).toList();// 2. 构建对话参数ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder().model(model).maxCompletionTokens(1000).tools(chatTools).addUserMessage(query);// 3. 首次调用(可能包含 function_call)ChatCompletion initial = openaiClient.chat().completions().create(builder.build());// 4. 处理模型回复List<ChatCompletion.Choice> choices = initial.choices();if (choices.isEmpty()) {return"[Error] empty response from model";}ChatCompletion.Choice first = choices.get(0);// 如果模型触发了 function_callwhile (first.finishReason().equals(TOOL_CALLS)) {ChatCompletionMessage msg = first.message();// 如果同时触发了多个工具调用,toolCalls() 会返回一个列表List<ChatCompletionMessageToolCall> calls = msg.toolCalls() // Optional<List<...>>// 若无调用则空列表.orElse(List.of());builder.addMessage(msg);for (ChatCompletionMessageToolCall call : calls) {ChatCompletionMessageToolCall.Function fn = call.function();// 执行 MCP 工具String toolResult = callMcpTool(fn.name(), fn.arguments());logger.info("Tool {} returned: {}", fn.name(), toolResult);// 将 function_call 与工具执行结果注入上下文builder.addMessage(ChatCompletionToolMessageParam.builder().toolCallId(Objects.requireNonNull(msg.toolCalls().orElse(null)).get(0).id()).content(toolResult).build());}// 5. 二次调用,拿最终回复ChatCompletion followup = openaiClient.chat().completions().create(builder.build());first = followup.choices().get(0);}// 若未触发函数调用,直接返回文本return first.message().content().orElse("无返回文本");} catch (Exception e) {logger.error("Unexpected error during processQuery", e);return"[Error] " + e.getMessage();}}/*** 调用 MCP Server 上的工具并返回结果文本*/private String callMcpTool(String name, String arguments){try {McpSchema.CallToolRequest req = new McpSchema.CallToolRequest(name, arguments);return toolToClient.get(name).callTool(req).content().stream().map(Object::toString).collect(Collectors.joining("\n"));} catch (Exception e) {logger.error("Failed to call MCP tool {}: {}", name, e.getMessage());return"[Tool Error] " + e.getMessage();}}}
package com.alibaba.damo.mcpclient.client;import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.core.JsonValue;import com.openai.models.FunctionDefinition;import com.openai.models.FunctionParameters;import com.openai.models.chat.completions.*;import io.modelcontextprotocol.client.McpClient;import io.modelcontextprotocol.client.McpSyncClient;import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;import io.modelcontextprotocol.spec.McpClientTransport;import io.modelcontextprotocol.spec.McpSchema;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.stream.Collectors;importstatic com.openai.models.chat.completions.ChatCompletion.Choice.FinishReason.TOOL_CALLS;/*** @author clong*/publicclassMyMCPClient {privatestaticfinal Logger logger = LoggerFactory.getLogger(MyMCPClient.class);("${spring.ai.openai.base-url}")private String baseUrl;("${spring.ai.openai.api-key}")private String apiKey;("${spring.ai.openai.chat.options.model}")private String model;// Tool 名称到 MCP Client 的映射privatefinal Map<String, McpSyncClient> toolToClient = new HashMap<>();("${mcp.servers}") // e.g. tool1=http://url1,tool2=http://url2,...private String toolServerMapping;private OpenAIClient openaiClient;privatefinal List<McpSchema.Tool> allTools = new ArrayList<>();publicvoidinit(){// 解析配置并初始化各 MCP ClientArrays.stream(toolServerMapping.split(",")).map(entry -> entry.split("=")).forEach(pair -> {String url = pair[1];McpClientTransport transport =HttpClientSseClientTransport.builder(url).build();McpSyncClient client = McpClient.sync(transport).build();client.initialize(); // 建立 SSE 连接logger.info("Connected to MCP server via SSE at {}", url);// 列出并打印所有可用工具List<McpSchema.Tool> tools = client.listTools().tools();logger.info("Available MCP tools:");tools.forEach(t -> logger.info(" - {} : {}", t.name(), t.description()));allTools.addAll(tools);tools.forEach(t -> toolToClient.put(t.name(), client));});// 2. 初始化 OpenAI 客户端this.openaiClient = OpenAIOkHttpClient.builder().baseUrl(baseUrl).apiKey(apiKey).checkJacksonVersionCompatibility(false).build();logger.info("OpenAI client initialized with model {}", model);}publicvoidshutdown(){// 如果有必要,优雅关闭 MCP 客户端toolToClient.values().forEach((client) -> {try {client.close();logger.info("Closed MCP client for {}", client);} catch (Exception e) {logger.warn("Error closing MCP client for {}: {}", client, e.getMessage());}});}/*** 处理一次用户查询:注入所有工具定义 -> 发首轮请求 -> 若触发 function_call 则执行 ->* 再次发请求获取最终回复*/public String processQuery(String query){try {List<ChatCompletionTool> chatTools = allTools.stream().map(t -> ChatCompletionTool.builder().function(FunctionDefinition.builder().name(t.name()).description(t.description()).parameters(FunctionParameters.builder().putAdditionalProperty("type", JsonValue.from(t.inputSchema().type())).putAdditionalProperty("properties", JsonValue.from(t.inputSchema().properties())).putAdditionalProperty("required", JsonValue.from(t.inputSchema().required())).putAdditionalProperty("additionalProperties", JsonValue.from(t.inputSchema().additionalProperties())).build()).build()).build()).toList();// 2. 构建对话参数ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder().model(model).maxCompletionTokens(1000).tools(chatTools).addUserMessage(query);// 3. 首次调用(可能包含 function_call)ChatCompletion initial = openaiClient.chat().completions().create(builder.build());// 4. 处理模型回复List<ChatCompletion.Choice> choices = initial.choices();if (choices.isEmpty()) {return"[Error] empty response from model";}ChatCompletion.Choice first = choices.get(0);// 如果模型触发了 function_callwhile (first.finishReason().equals(TOOL_CALLS)) {ChatCompletionMessage msg = first.message();// 如果同时触发了多个工具调用,toolCalls() 会返回一个列表List<ChatCompletionMessageToolCall> calls = msg.toolCalls() // Optional<List<...>>// 若无调用则空列表.orElse(List.of());builder.addMessage(msg);for (ChatCompletionMessageToolCall call : calls) {ChatCompletionMessageToolCall.Function fn = call.function();// 执行 MCP 工具String toolResult = callMcpTool(fn.name(), fn.arguments());logger.info("Tool {} returned: {}", fn.name(), toolResult);// 将 function_call 与工具执行结果注入上下文builder.addMessage(ChatCompletionToolMessageParam.builder().toolCallId(Objects.requireNonNull(msg.toolCalls().orElse(null)).get(0).id()).content(toolResult).build());}// 5. 二次调用,拿最终回复ChatCompletion followup = openaiClient.chat().completions().create(builder.build());first = followup.choices().get(0);}// 若未触发函数调用,直接返回文本return first.message().content().orElse("无返回文本");} catch (Exception e) {logger.error("Unexpected error during processQuery", e);return"[Error] " + e.getMessage();}}/*** 调用 MCP Server 上的工具并返回结果文本*/private String callMcpTool(String name, String arguments){try {McpSchema.CallToolRequest req = new McpSchema.CallToolRequest(name, arguments);return toolToClient.get(name).callTool(req).content().stream().map(Object::toString).collect(Collectors.joining("\n"));} catch (Exception e) {logger.error("Failed to call MCP tool {}: {}", name, e.getMessage());return"[Tool Error] " + e.getMessage();}}}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.alibaba.damo</groupId><artifactId>mcp-client</artifactId><version>0.0.1-SNAPSHOT</version><name>mcp-client</name><description>mcp-client</description><properties><java.version>17</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>3.4.0</spring-boot.version><mcp.version>0.9.0</mcp.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>${spring-boot.version}</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp</artifactId><version>0.9.0</version></dependency><dependency><groupId>com.openai</groupId><artifactId>openai-java</artifactId><version>1.6.0</version></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>1.0.0-M7</version><type>pom</type><scope>import</scope></dependency><!-- MCP BOM 统一版本 --><dependency><groupId>io.modelcontextprotocol.sdk</groupId><artifactId>mcp-bom</artifactId><version>${mcp.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>17</source><target>17</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.alibaba.damo.mcpclient.McpClientApplication</mainClass><skip>true</skip></configuration><executions><execution><id>repackage</id><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></build></project>
spring.ai.openai.api-key=sk-xxxxspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1spring.ai.openai.chat.options.model=qwen-maxmcp.servers=blender=http://localhost:8000
下面重点梳理 processQuery 方法中的 核心逻辑流程,可分为三大步骤:
allTools(从所有 MCP Server 拉取到的工具列表),把每个工具的名称、描述和输入参数 JSON Schema 封装成 OpenAI SDK 可识别的 FunctionDefinition 对象;ChatCompletionCreateParams 时,将这些 FunctionDefinition 作为 tools 传入,告诉模型“你可以调这些外部工具”。gpt-4o-mini)、最大 token 限制和用户提问 query,调用 openaiClient.chat().completions().create(...);initial,其中可能包含:function_call,即模型决定调用某个工具来获取更准确的数据。while (first.finishReason() == TOOL_CALLS) {  1. 从模型回复中提取 function_call(函数名 + 参数 JSON)  2. 调用 callMcpTool(name, args):把请求发给对应的 MCP Server,同步拿回执行结果文本  3. 将模型的 function_call 消息和工具执行结果消息依次注入对话上下文(builder.addMessage(...))  4. 用更新后的上下文,再次调用 openaiClient.chat().completions().create(...),获取新的 `first`}function_call,而是以纯文本的形式给出最终响应;first.message().content() 作为完整回答返回。核心优势:
启动springboot服务,通过controller接口post请求测试。
publicclassController {private MyMCPClient myMCPClient;("/client")public String client(("query") String query){return myMCPClient.processQuery(query);}}
client端日志:
2025-05-07T17:35:42.974+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool get_scene_info returned: TextContent[audience=null, priority=null, text={  "name": "Scene",  "object_count": 3,  "objects": [    {      "name": "WaterCup",      "type": "MESH",      "location": [        0.0,        0.0,        0.0      ]    },    {      "name": "Camera",      "type": "CAMERA",      "location": [        4.0,        -4.0,        3.0      ]    },    {      "name": "MainLight",      "type": "LIGHT",      "location": [        2.0,        -2.0,        3.0      ]    }  ],  "materials_count": 12}]2025-05-07T17:35:46.371+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool get_hyper3d_status returned: TextContent[audience=null, priority=null, text=Hyper3D Rodin integration is currently disabled. To enable it:1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)2. Check the 'Use Hyper3D Rodin 3D model generation' checkbox3. Restart the connection to Claude]2025-05-07T17:35:50.276+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool delete_object returned: TextContent[audience=null, priority=null, text=Deleted object: WaterCup]2025-05-07T17:35:54.286+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool delete_object returned: TextContent[audience=null, priority=null, text=Deleted object: Camera]2025-05-07T17:35:58.516+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool delete_object returned: TextContent[audience=null, priority=null, text=Deleted object: MainLight]2025-05-07T17:36:23.889+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool execute_blender_code returned: TextContent[audience=null, priority=null, text=Code executed successfully: ]2025-05-07T17:36:27.330+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool save_scene returned: TextContent[audience=null, priority=null, text=Scene saved to /Users/clong/Pictures/pig.blend]server端部分核心日志
2025-05-0717:36:23,716 - BlenderMCPServer - INFO - Sending command: execute_code with params: {'code': 'import bpy\nimport math\n\n# Create the pig\'s body\nbpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(0, 0, 0.8))\nbody = bpy.context.active_object\nbody.name = "Pig_Body"\nbody.scale = (1.5, 1, 0.8)\n\n# Create the pig\'s head\nbpy.ops.mesh.primitive_uv_sphere_add(radius=0.7, location=(1.5, 0, 1.2))\nhead = bpy.context.active_object\nhead.name = "Pig_Head"\n\n# Create the pig\'s snout\nbpy.ops.mesh.primitive_cylinder_add(radius=0.3, depth=0.4, location=(2.0, 0, 1.0))\nsnout = bpy.context.active_object\nsnout.name = "Pig_Snout"\nsnout.rotation_euler = (math.radians(90), 0, 0)\n\n# Create the nostrils\nbpy.ops.mesh.primitive_cylinder_add(radius=0.08, depth=0.1, location=(2.2, 0.15, 1))\nleft_nostril = bpy.context.active_object\nleft_nostril.name = "Pig_Nostril_Left"\nleft_nostril.rotation_euler = (math.radians(90), 0, 0)\n\nbpy.ops.mesh.primitive_cylinder_add(radius=0.08, depth=0.1, location=(2.2, -0.15, 1))\nright_nostril = bpy.context.active_object\nright_nostril.name = "Pig_Nostril_Right"\nright_nostril.rotation_euler = (math.radians(90), 0, 0)\n\n# Create the eyes\nbpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(1.9, 0.3, 1.5))\nleft_eye = bpy.context.active_object\nleft_eye.name = "Pig_Eye_Left"\n\nbpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(1.9, -0.3, 1.5))\nright_eye = bpy.context.active_object\nright_eye.name = "Pig_Eye_Right"\n\n# Create the ears\nbpy.ops.mesh.primitive_cone_add(radius1=0.3, radius2=0, depth=0.5, location=(1.3, 0.5, 1.8))\nleft_ear = bpy.context.active_object\nleft_ear.name = "Pig_Ear_Left"\nleft_ear.rotation_euler = (math.radians(-30), math.radians(-20), math.radians(20))\n\nbpy.ops.mesh.primitive_cone_add(radius1=0.3, radius2=0, depth=0.5, location=(1.3, -0.5, 1.8))\nright_ear = bpy.context.active_object\nright_ear.name = "Pig_Ear_Right"\nright_ear.rotation_euler = (math.radians(-30), math.radians(20), math.radians(-20))\n\n# Create the legs\ndef create_leg(name, x, y):\n    bpy.ops.mesh.primitive_cylinder_add(radius=0.2, depth=0.7, location=(x, y, 0.3))\n    leg = bpy.context.active_object\n    leg.name = name\n    return leg\n\nfront_left_leg = create_leg("Pig_Leg_Front_Left", 0.7, 0.5)\nfront_right_leg = create_leg("Pig_Leg_Front_Right", 0.7, -0.5)\nback_left_leg = create_leg("Pig_Leg_Back_Left", -0.7, 0.5)\nback_right_leg = create_leg("Pig_Leg_Back_Right", -0.7, -0.5)\n\n# Create the tail\nbpy.ops.curve.primitive_bezier_curve_add(location=(-1.5, 0, 0.8))\ntail = bpy.context.active_object\ntail.name = "Pig_Tail"\n\n# Modify the tail curve\ntail.data.bevel_depth = 0.05\ntail.data.bevel_resolution = 4\ntail.data.fill_mode = \'FULL\'\n\n# Set the curve points\npoints = tail.data.splines[0].bezier_points\npoints[0].co = (-1.5, 0, 0.8)\npoints[0].handle_left = (-1.5, -0.2, 0.7)\npoints[0].handle_right = (-1.5, 0.2, 0.9)\npoints[1].co = (-1.8, 0.3, 1.0)\npoints[1].handle_left = (-1.7, 0.1, 1.0)\npoints[1].handle_right = (-1.9, 0.5, 1.0)\n\n# Create the main pig material\npig_mat = bpy.data.materials.new(name="Pig_Material")\npig_mat.diffuse_color = (0.9, 0.6, 0.6, 1.0)\n\n# Create the eye material\neye_mat = bpy.data.materials.new(name="Eye_Material")\neye_mat.diffuse_color = (0.0, 0.0, 0.0, 1.0)\n\n# Create the nostril material\nnostril_mat = bpy.data.materials.new(name="Nostril_Material")\nnostril_mat.diffuse_color = (0.2, 0.0, 0.0, 1.0)\n\n# Apply materials\nfor obj in bpy.data.objects:\n    if obj.name.startswith("Pig_") andnot obj.name.startswith("Pig_Eye") andnot obj.name.startswith("Pig_Nostril"):\n        if obj.data.materials:\n            obj.data.materials[0] = pig_mat\n        else:\n            obj.data.materials.append(pig_mat)\n    \n    if obj.name.startswith("Pig_Eye"):\n        if obj.data.materials:\n            obj.data.materials[0] = eye_mat\n        else:\n            obj.data.materials.append(eye_mat)\n    \n    if obj.name.startswith("Pig_Nostril"):\n        if obj.data.materials:\n            obj.data.materials[0] = nostril_mat\n        else:\n            obj.data.materials.append(nostril_mat)\n\n# Create a new collection for the pig\npig_collection = bpy.data.collections.new("Pig")\nbpy.context.scene.collection.children.link(pig_collection)\n\n# Add all pig objects to the collection\nfor obj in bpy.data.objects:\n    if obj.name.startswith("Pig_"):\n        # First remove from current collection\n        for coll in obj.users_collection:\n            coll.objects.unlink(obj)\n        # Then add to the pig collection\n        pig_collection.objects.link(obj)\n\n# Add a new camera\nbpy.ops.object.camera_add(location=(5, -5, 3))\ncamera = bpy.context.active_object\ncamera.name = "Camera"\ncamera.rotation_euler = (math.radians(60), 0, math.radians(45))\nbpy.context.scene.camera = camera\n\n# Add a light\nbpy.ops.object.light_add(type=\'SUN\', location=(5, 5, 10))\nlight = bpy.context.active_object\nlight.name = "Sun"\nlight.rotation_euler = (math.radians(45), math.radians(45), 0)'}2025-05-07 17:36:23,718 - BlenderMCPServer - INFO - Command sent, waiting for response...2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Received complete response (51 bytes)2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Received 51 bytes of data2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Response parsed, status: success2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Response result: {'executed': True}INFO:     127.0.0.1:51875 - "POST /messages/?session_id=fa4b43638d574203b05d40cd283c78cf HTTP/1.1"202 Accepted2025-05-0717:36:27,020 - mcp.server.lowlevel.server - INFO - Processing request of type CallToolRequest2025-05-0717:36:27,020 - BlenderMCPServer - INFO - Sending command: get_polyhaven_status with params: None2025-05-0717:36:27,021 - BlenderMCPServer - INFO - Command sent, waiting for response...2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Received complete response (115 bytes)2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Received 115 bytes of data2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Response parsed, status: success2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Response result: {'enabled': True, 'message': 'PolyHaven integration is enabled and ready to use.'}2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Sending command: save_scene with params: {'filepath': '/Users/clong/Pictures/pig.blend'}2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Command sent, waiting for response...2025-05-0717:36:27,326 - BlenderMCPServer - INFO - Received complete response (80 bytes)2025-05-0717:36:27,327 - BlenderMCPServer - INFO - Received 80 bytes of data2025-05-0717:36:27,327 - BlenderMCPServer - INFO - Response parsed, status: success2025-05-0717:36:27,327 - BlenderMCPServer - INFO - Response result: {'filepath': '/Users/clong/Pictures/pig.blend'}已经保存到本地指定目录下
使用blender打开即可看到一头粉色的小猪
结语
本文系统性地探索了 Java 开发者在 MCP(Model Context Protocol)生态中的实践路径,从背景调研到技术验证,从 Spring AI 的局限性到原生 MCP SDK 的深度整合,最终实现了 Java 客户端与 Python 服务端的无缝协作。这一过程不仅验证了 MCP 协议的跨语言通用性,也为企业级 Java 应用对接主流 AI 工具链提供了可复用的解决方案。
未来展望
致开发者:拥抱协议,而非绑定技术栈
MCP 的核心价值在于定义标准化接口,解耦模型与工具。对 Java 开发者而言,无需受限于 Python 主导的 MCP 生态,而应聚焦于协议本身的工程化落地。通过本文的实践,我们已证明:Java 同样可以成为 AI 工具链的“连接器”,为复杂业务场景提供稳定、高效的支持。
未来,随着 MCP 协议的演进与多语言 SDK 的成熟,跨生态协作将成为 AI 应用开发的常态。期待更多开发者加入这一探索,共同构建开放、兼容、可扩展的智能系统。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2025-10-31
有人问我会不会用 AI,我直接拿出这个 Ollama + FastGPT 项目给他看
2025-10-30
开源可信MCP,AICC机密计算新升级!
2025-10-30
OpenAI 开源了推理安全模型-gpt-oss-safeguard-120b 和 gpt-oss-safeguard-20b
2025-10-29
刚刚,OpenAI 再次开源!安全分类模型 gpt-oss-safeguard 准确率超越 GPT-5
2025-10-29
AI本地知识库+智能体系列:手把手教你本地部署 n8n,一键实现自动采集+智能处理!
2025-10-29
n8n如何调用最近爆火的deepseek OCR?
2025-10-29
OpenAI终于快要上市了,也直面了这23个灵魂拷问。
2025-10-29
保姆级教程:我用Coze干掉了最烦的周报
 
            2025-08-20
2025-09-07
2025-08-05
2025-08-20
2025-08-26
2025-08-22
2025-09-06
2025-08-06
2025-10-20
2025-08-22
2025-10-29
2025-10-28
2025-10-13
2025-09-29
2025-09-17
2025-09-09
2025-09-08
2025-09-07