|
|
捣蛋的手术刀 · Mysql4种方式避免重复插入数据!-腾讯云 ...· 2 年前 · |
|
|
彷徨的仙人掌 · vue中,svg图标的click事件不生效 ...· 2 年前 · |
|
|
含蓄的香槟 · python中类的相互调用的实践_pytho ...· 2 年前 · |
|
|
眉毛粗的柳树 · Spring Security ...· 2 年前 · |
目前主流的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 客户端和 MCP 服务器实现支持:
协议版本兼容性协商[1]
工具[2]发现、执行、列表变更通知
使用 URI 模板进行资源[3]管理
Roots[4]列表管理和通知
Prompt[5]处理和管理
对 AI 模型交互的采样[6]支持
多种传输实现:
默认传输(包含在核心mcp模块中,不需要外部 Web 框架):
基于 Stdio 的传输,用于基于进程的通信;
基于 Java HttpClient 的 SSE 客户端传输,用于 HTTP SSE 客户端流;
基于 Servlet 的 SSE 服务器传输,用于 HTTP SSE 服务器流;
可选的基于 Spring 的传输(如果使用 Spring 框架则很方便):
WebFlux SSE 客户端和服务器传输用于响应式 HTTP 流;
用于基于 servlet 的 HTTP 流的 WebMVC SSE 传输;
支持同步和异步编程范例
核心io.modelcontextprotocol.sdk:mcp模块提供默认的 STDIO 和 SSE 客户端和服务器传输实现,而无需外部 Web 框架。
为了方便使用 Spring [7]框架,Spring 特定的传输可作为可选依赖项使用。
SDK 遵循分层架构,关注点清晰分离:
客户端/服务器层(McpClient/McpServer):两者都使用 McpSession 进行同步/异步操作,其中 McpClient 处理客户端协议操作,McpServer 管理服务器端协议操作。
会话层(McpSession):使用 DefaultMcpSession 实现管理通信模式和状态。
传输层(McpTransport):通过以下方式处理 JSON-RPC 消息序列化/反序列化:
核心模块中的 StdioTransport (stdin/stdout);
专用传输模块(Java HttpClient、Spring WebFlux、Spring WebMVC)中的 HTTP SSE 传输。
MCP 客户端是模型上下文协议 (MCP) 架构中的关键组件,负责建立和管理与 MCP 服务器的连接。它实现了协议的客户端功能。
MCP 服务器是模型上下文协议 (MCP) 架构中的基础组件,为客户端提供工具、资源和功能。它实现了协议的服务器端。主要作用:
客户端/服务器初始化:传输设置、协议兼容性检查、能力协商和实现细节交换。
消息流:JSON-RPC 消息处理,带有验证、类型安全响应处理和错误处理。
资源管理:资源发现、基于 URI 模板的访问、订阅系统和内容检索。
核心 MCP 功能:
<dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> </dependency>
<!-- 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 版本决定了所使用的依赖项版本。这还能确保默认使用受支持且经过测试的依赖项版本,除非选择覆盖这些版本。
将 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>
核心依赖项
io.modelcontextprotocol.sdk:mcp- 核心 MCP 库提供模型上下文协议
(MCP) 实现的基本功能和 API,包括默认的 STDIO 和 SSE 客户端及服务器传输实现。无需任何外部
Web 框架。
可选的传输依赖项(如果使用 Spring 框架则很方便)
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(标准输入/输出) 。
1. 定义
SSE(Server-Sent Events)
基于 HTTP/1.1 的单向推送技术。客户端(浏览器或其他 HTTP 客户端)通过发起一个带 Accept:
text/event-stream 的 GET 请求,与服务器建立一个持久化的连接,服务器可以随时把事件流(文本格式)推送到客户端,客户端通过
JavaScript 的 EventSource API 监听并处理。
STDIO(Standard I/O)
操作系统中进程的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。是进程级的字节流接口,用于命令行程序或脚本间的数据传递,典型用法是管道(|)、重定向(>/<)等。
2. 通信模型
3. 典型使用场景
实时推送:股票行情、微博推送、新消息提示等,需要浏览器端实时更新的场景;
简化实现:只需 HTTP,不需要 WebSocket 的握手和多路复用;
STDIO
命令行工具:如 grep、sed、ffmpeg 等通过管道串联,快速处理文本或二进制流;
脚本自动化:Shell 脚本或进程间的简单数据传输;
4. 优缺点对比
如果你的目标是 在浏览器或 HTTP 客户端中,需要 服务器主动推送 新事件,且希望自动重连和统一走
HTTP/HTTPS,选 SSE 最合适;
如果你在 命令行 或 本地进程 间做 高速流式数据处理、管道拼接,并不依赖网络协议,STDIO 是最自然也最高效的选择。
server
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
@Service
public class OpenMeteoService {
private final WebClient webClient;
public OpenMeteoService(WebClient.Builder webClientBuilder) {
this.webClient = webClientBuilder
.baseUrl("https://api.open-meteo.com/v1")
.build();
@Tool(description = "根据经纬度获取天气预报")
public String getWeatherForecastByLocation(
@ToolParam(description = "纬度,例如:39.9042") String latitude,
@ToolParam(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();
@Tool(description = "根据经纬度获取空气质量信息")
public String getAirQuality(
@ToolParam(description = "纬度,例如:39.9042") String latitude,
@ToolParam(description = "经度,例如:116.4074") String longitude) {
// 模拟数据,实际应用中应调用真实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;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
@Bean
public ToolCallbackProvider weatherTools(OpenMeteoService openMeteoService) {
return MethodToolCallbackProvider.builder()
.toolObjects(openMeteoService)
.build();
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
<?xml version="1.0" encoding="UTF-8"?>
<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 1 default profile: "default"
2025-04-29T20:09:07.561+08:00 INFO 51478 --- [ main]
o.s.a.m.s.a.McpServerAutoConfiguration : Registered tools: 2, notification: true
2025-04-29T20:09:07.609+08:00 INFO 51478 --- [ main]
o.s.b.web.embedded.netty.NettyWebServer : Netty started on port 8080
2025-04-29T20:09:07.612+08:00 INFO 51478 --- [ main]
c.a.damo.mcpserver.McpServerApplication : Started McpServerApplication in 0.572 seconds (process running for 0.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]
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;
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
@Bean
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
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;
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
@Bean
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();
依赖中需要添加spring-ai-starter-mcp-client依赖:
<?xml version="1.0" encoding="UTF-8"?>
<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 1 default 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 for 1.083)
>>> QUESTION: 杭州今天天气如何?
>>> ASSISTANT: 杭州当前的天气信息如下:
- 温度:24.4°C
- 风速:3.4 km/h
请注意,这些信息是基于当前时间的实时数据。
STDIO
server
与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文件。
client
客户端增加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"
<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>-->
日志同上,不再打印。
重要配置项解析
spring.main.web-application-type=none
1. Spring Boot 自动配置与 WebApplicationType
Spring Boot 在启动时会根据类路径自动检测应用类型(WebApplicationType),并加载对应的自动配置:
若检测到 WebFlux 相关依赖,则创建 ReactiveWebApplicationContext,并尝试注册
ReactiveWebServerFactory;
若检测到 Servlet(Spring MVC)相关依赖,则创建 :
ServletWebServerApplicationContext,并尝试注册 ServletWebServerFactory;
若未检测到任何 Web 依赖,或显式设置为 NONE,则不会初始化任何内嵌 Web 容器。
2. 缺少对应的 Starter 依赖
缺少 WebFlux Starter:若项目未引入 spring-boot-starter-webflux,则不会创建
ReactiveWebServerFactory,导致启动 ReactiveWebApplicationContext
时抛出缺失 Bean 异常;
缺少 Servlet Starter:同理,若项目未引入 spring-boot-starter-web,则不会创建
ServletWebServerFactory,会在启动 ServletWebServerApplicationContext
时抛出类似错误。
3. 应用类型与 Starter 冲突
当同时引入了 spring-web(Servlet)和 spring-webflux(Reactive)依赖时,Spring
Boot 默认优先选择 Servlet 模式;若业务需要 Reactive,可显式设置 spring.main.web-application-type=reactive,否则仍然会走
Servlet 自动配置路径。
因此我们需要将该配置项设置为none,避免WebFlux或者Servlet容器报找不到错误。
spring.main.banner-mode=off
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=true
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 的语言无关性
1.1 基于 JSON-RPC 2.0
MCP 的底层通信协议是 JSON-RPC 2.0,它使用纯文本的 JSON 作为编码格式,极大地保证了跨语言互操作性。任何能读写
JSON 并通过 TCP/STDIO/HTTP/WebSocket 等传输层发送、接收文本的语言,都能实现对
MCP 消息的编解码。
1.2 官方多语言 SDK
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服务端。
3.1 Spring AI
如果使用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
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:
) -> 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)
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}")
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)
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
@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();
<?xml version="1.0" encoding="UTF-8"?>
<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 方法中的 核心逻辑流程,可分为三大步骤:
1. 将 MCP 工具注册为 OpenAI Function
遍历 allTools(从所有 MCP Server 拉取到的工具列表),把每个工具的名称、描述和输入参数
JSON Schema 封装成 OpenAI SDK 可识别的 FunctionDefinition
构造 ChatCompletionCreateParams 时,将这些 FunctionDefinition
作为 tools 传入,告诉模型“你可以调这些外部工具”。
2. 首次发起 ChatCompletion 请求
使用指定的模型(如 gpt-4o-mini)、最大 token 限制和用户提问 query,调用 openaiClient.chat().completions().create(...);
获得模型的初步回复 initial,其中可能包含:
直接的文本回答;
或者一个 function_call,即模型决定调用某个工具来获取更准确的数据。
3. 循环处理 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() 作为完整回答返回。
核心优势:
动态工具调用:让 LLM 在对话中主动“点”外部工具,获取实时、可信的数据;
对话式编排:多轮注入上下文,不丢失模型的思考链路,保证回答连贯;
解耦清晰:把“模型对话”和“工具执行”分离,用循环机制优雅衔接。
启动springboot服务,通过controller接口post请求测试。
@RestController
publicclassController {
@Autowired
private MyMCPClient myMCPClient;
@PostMapping("/client")
public String client(@RequestParam("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": [
"name": "Camera",
"type": "CAMERA",
"location": [
-4.0,
"name": "MainLight",
"type": "LIGHT",
"location": [
-2.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'
checkbox
3. 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 data
2025-05-07 17:36:23,884 - BlenderMCPServer -
INFO - Response parsed, status: success
2025-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 Accepted
2025-05-0717:36:27,020 - mcp.server.lowlevel.server
- INFO - Processing request of type CallToolRequest
2025-05-0717:36:27,020 - BlenderMCPServer -
INFO - Sending command: get_polyhaven_status
with params: None
2025-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 data
2025-05-0717:36:27,121 - BlenderMCPServer -
INFO - Response parsed, status: success
2025-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 data
2025-05-0717:36:27,327 - BlenderMCPServer -
INFO - Response parsed, status: success
2025-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 工具链提供了可复用的解决方案。
1. Spring AI 的局限性
当前 Spring AI 对 MCP 的封装存在兼容性问题(如空请求体导致的反序列化失败),需社区推动完善
SDK 实现,或依赖更灵活的原生库。
2. MCP 生态的标准化
尽管 MCP 协议本身语言无关,但各语言 SDK 的实现细节(如传输层、错误处理)仍需统一规范,以降低跨语言协作成本。
3. 企业级应用扩展
性能优化:探索 WebSocket 替代 SSE 以支持双向实时通信,提升高并发场景下的效率。
安全增强:引入 TLS 加密、身份认证(OAuth/JWT)保障服务间通信安全。
可观测性:集成日志追踪(如 OpenTelemetry)与指标监控,提升系统运维能力。
4. 社区共建与工具链完善
MCP 的普及依赖于多语言工具链的丰富性。Java 社区可贡献更多开箱即用的 MCP 工具库,并推动与主流框架(如
Spring Cloud、Quarkus)的深度集成。
致开发者:拥抱协议,而非绑定技术栈
MCP 的核心价值在于定义标准化接口,解耦模型与工具。对 Java 开发者而言,无需受限于 Python
主导的 MCP 生态,而应聚焦于协议本身的工程化落地。通过本文的实践,我们已证明:Java 同样可以成为
AI 工具链的“连接器”,为复杂业务场景提供稳定、高效的支持。
未来,随着 MCP 协议的演进与多语言 SDK 的成熟,跨生态协作将成为 AI 应用开发的常态。期待更多开发者加入这一探索,共同构建开放、兼容、可扩展的智能系统。