微信扫码
添加专属顾问
我要投稿
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(
"纬度,例如:39.9042") String latitude, (description =
"经度,例如:116.4074") String longitude) { (description =
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(
"纬度,例如:39.9042") String latitude, (description =
"经度,例如:116.4074") String longitude) { (description =
// 模拟数据,实际应用中应调用真实API
return"当前位置(纬度:" + 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=8080
spring.ai.mcp.server.name=my-weather-server
spring.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-xxx
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
spring.ai.openai.chat.options.model=qwen-max
spring.ai.mcp.client.toolcallback.enabled=true
spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
spring.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 null
2025-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=8080
spring.main.web-application-type=none
spring.main.banner-mode=off
spring.ai.mcp.server.stdio=true
spring.ai.mcp.server.name=my-weather-server
spring.ai.mcp.server.version=0.0.1
修改完之后通过maven package打包成jar文件。
客户端增加mcp-servers-config.json配置路径,启用toolcallback,注释掉sse连接。
spring.ai.openai.api-key=sk-XXXXXX
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
spring.ai.openai.chat.options.model=qwen-max
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.json
spring.ai.mcp.client.toolcallback.enabled=true
#spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
spring.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 OK
WARNING: Unsupported upgrade request.
INFO: 127.0.0.1:55087 - "POST /messages/?session_id=5b92a6377fcb4b3fa9f051b43d0379b5 HTTP/1.1"500 Internal Server Error
ERROR: Exception in ASGI application
Traceback(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_asgi
result = 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 exc
File "/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_app
raise exc
File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
await 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 app
await route.handle(scope, receive, send)
File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 460, in handle
await 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_message
json = await request.json()
^^^^^^^^^^^^^^^^^^^^
File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/requests.py", line 248, in json
self._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 loads
return _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 decode
obj, 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_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.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)
return
logger.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
*/
@Service
publicclassMyMCPClient {
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<>();
@PostConstruct
publicvoidinit(){
// 解析配置并初始化各 MCP Client
Arrays.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);
}
@PreDestroy
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_call
while (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 Client
Arrays.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_call
while (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-xxxx
spring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1
spring.ai.openai.chat.options.model=qwen-max
mcp.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-06-06
企业必备神器!零代码快速部署DeepSeek-R1-0528超强版本~
2025-06-06
基于MCP协议的12306购票搜索服务器项目解析(附配置流程)!
2025-06-06
性能大涨!阿里开源新版Qwen3模型,霸榜文本表征
2025-06-06
Qwen3新成员:Embedding系列模型登场!
2025-06-05
Dify限制太多?试试开源可商用的LLM开发平台:毕昇BISHENG
2025-06-05
微软开源!58K 星的 Office 文档转换神器,支持 MCP。
2025-06-05
智能体开发实战|基于Dify自定义工作流工具构建游戏智能体
2025-06-04
Dify DeepResearch 2.0 评测:告别玩具时代?Dify深度研究Agent究竟进化到哪一步了!
2024-07-25
2025-01-01
2025-01-21
2024-05-06
2024-09-20
2024-07-20
2024-07-11
2024-06-12
2024-12-26
2024-08-13
2025-05-28
2025-05-28
2025-05-26
2025-05-25
2025-05-23
2025-05-17
2025-05-17
2025-05-17