返回顶部
热门问答 更多热门问答
技术文章 更多技术文章

MCP Java 开发指南

[复制链接]
链载Ai 显示全部楼层 发表于 4 小时前 |阅读模式 打印 上一主题 下一主题

背景

目前主流的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>

核心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 版本决定了所使用的依赖项版本。这还能确保默认使用受支持且经过测试的依赖项版本,除非选择覆盖这些版本。

将 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 框架。
  • 可选的传输依赖项(如果使用 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)

SpringAI 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 的EventSourceAPI监听并处理。
  • STDIO(Standard I/O)

    操作系统中进程的标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。是进程级的字节流接口,用于命令行程序或脚本间的数据传递,典型用法是管道(|)、重定向(>/<)等。

2. 通信模型

3. 典型使用场景

SSE

  • 实时推送:股票行情、微博推送、新消息提示等,需要浏览器端实时更新的场景;
  • 简化实现:只需 HTTP,不需要 WebSocket 的握手和多路复用;

STDIO

  • 命令行工具:如grepsedffmpeg等通过管道串联,快速处理文本或二进制流;
  • 脚本自动化:Shell 脚本或进程间的简单数据传输;

4. 优缺点对比

总结:

  • 如果你的目标是在浏览器或 HTTP 客户端中,需要服务器主动推送新事件,且希望自动重连和统一走 HTTP/HTTPS,选SSE最合适;
  • 如果你在命令行本地进程间做高速流式数据处理管道拼接,并不依赖网络协议,STDIO是最自然也最高效的选择。

SSE

server

Service类:

package com.alibaba.damo.mcpserver.service;
importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.ai.tool.annotation.ToolParam;importorg.springframework.stereotype.Service;importorg.springframework.web.reactive.function.client.WebClient;
/***@authorclong*/@ServicepublicclassOpenMeteoService {
privatefinalWebClientwebClient;
publicOpenMeteoService(WebClient.Builder webClientBuilder){ this.webClient= webClientBuilder .baseUrl("https://api.open-meteo.com/v1") .build(); }
@Tool(description ="根据经纬度获取天气预报") publicStringgetWeatherForecastByLocation( @ToolParam(description ="纬度,例如:39.9042")Stringlatitude, @ToolParam(description ="经度,例如:116.4074")Stringlongitude) {
try{ Stringresponse = 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(Exceptione) { return"获取天气信息失败:"+ e.getMessage(); } }
@Tool(description ="根据经纬度获取空气质量信息") publicStringgetAirQuality( @ToolParam(description ="纬度,例如:39.9042")Stringlatitude, @ToolParam(description ="经度,例如:116.4074")Stringlongitude) {
// 模拟数据,实际应用中应调用真实API return"当前位置(纬度:"+ latitude +",经度:"+ longitude +")的空气质量:\n"+ "- PM2.5: 15 μg/m³ (优)\n"+ "- PM10: 28 μg/m³ (良)\n"+ "- 空气质量指数(AQI): 42 (优)\n"+ "- 主要污染物: 无"; }}

启动类:

package com.alibaba.damo.mcpserver;
importcom.alibaba.damo.mcpserver.service.OpenMeteoService;importorg.springframework.ai.tool.ToolCallbackProvider;importorg.springframework.ai.tool.method.MethodToolCallbackProvider;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;importorg.springframework.web.reactive.function.client.WebClient;
@SpringBootApplicationpublicclassMcpServerApplication {
publicstaticvoidmain(String[] args){ SpringApplication.run(McpServerApplication.class, args); }
@Bean publicToolCallbackProviderweatherTools(OpenMeteoService openMeteoService){ returnMethodToolCallbackProvider.builder() .toolObjects(openMeteoService) .build(); }
@Bean publicWebClient.BuilderwebClientBuilder(){ returnWebClient.builder(); }
}

配置文件:

server.port=8080
spring.ai.mcp.server.name=my-weather-serverspring.ai.mcp.server.version=0.0.1

依赖:

<?xml version="1.0"encoding="UTF-8"?><projectxmlns="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:00INFO51478---[main]c.a.damo.mcpserver.McpServerApplication:StartingMcpServerApplicationusingJava17.0.15withPID51478(/Users/clong/IdeaProjects/mcp-server/target/classesstartedbyclongin/Users/clong/IdeaProjects/mcp-server)2025-04-29T20:09:07.154+08:00INFO51478---[main]c.a.damo.mcpserver.McpServerApplication:Noactiveprofileset,fallingbackto1defaultprofile:"default"2025-04-29T20:09:07.561+08:00INFO51478---[main]o.s.a.m.s.a.McpServerAutoConfiguration:Registeredtools:2,notification:true2025-04-29T20:09:07.609+08:00INFO51478---[main]o.s.b.web.embedded.netty.NettyWebServer:Nettystartedonport80802025-04-29T20:09:07.612+08:00INFO51478---[main]c.a.damo.mcpserver.McpServerApplication:StartedMcpServerApplicationin0.572seconds(processrunningfor0.765)2025-04-29T20:10:18.812+08:00INFO51478---[ctor-http-nio-3]i.m.server.McpAsyncServer:Clientinitializerequest-Protocol:2024-11-05,Capabilities:ClientCapabilities[experimental=null,roots=null,sampling=null],Info:Implementation[name=spring-ai-mcp-client-server1,version=1.0.0]

client

客户端只需要在启动类中构建ChatClient并注入MCP工具即可。

package com.alibaba.damo.mcpclient;
importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.tool.ToolCallbackProvider;importorg.springframework.boot.CommandLineRunner;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.context.annotation.Bean;
importjava.util.Arrays;
@SpringBootApplicationpublicclassMcpClientApplication {
publicstaticvoidmain(String[] args){ SpringApplication.run(McpClientApplication.class, args); }
@Bean publicCommandLineRunnerpredefinedQuestions( ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools, ConfigurableApplicationContext context) { returnargs -> { // 构建ChatClient并注入MCP工具 varchatClient = chatClientBuilder .defaultTools(tools) .build();
// 定义用户输入 StringuserInput ="杭州今天天气如何?"; // 打印问题 System.out.println("\n>>> QUESTION: "+ userInput); // 调用LLM并打印响应 System.out.println("\n>>> ASSISTANT: "+ chatClient.prompt(userInput).call().content());
// 关闭应用上下文 context.close(); }; }
}

阿里云百炼平台提供各大模型百万token免费体验,可以直接去平台申请即可获取对应的sk。

https://bailian.console.aliyun.com/console?tab=api#/api

spring.ai.openai.api-key=sk-xxxspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1spring.ai.openai.chat.options.model=qwen-max
spring.ai.mcp.client.toolcallback.enabled=truespring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080
spring.main.web-application-type=none

依赖中需要添加spring-ai-starter-mcp-client依赖:

<?xml version="1.0"encoding="UTF-8"?><projectxmlns="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 INFO51843--- [      main] c.a.damo.mcpclient.McpClientApplication : Starting McpClientApplicationusingJava17.0.15withPID51843(/Users/clong/IdeaProjects/mcp-client/target/classes startedbyclongin/Users/clong/IdeaProjects/mcp-client)2025-04-29T20:10:18.303+08:00 INFO51843--- [      main] c.a.damo.mcpclient.McpClientApplication : No active profileset, falling back to1defaultprofile:"default"2025-04-29T20:10:18.846+08:00 INFO51843--- [ient-1-Worker-0] i.m.client.McpAsyncClient        : Server responsewithProtocol: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]andInstructionsnull2025-04-29T20:10:19.018+08:00 INFO51843--- [      main] c.a.damo.mcpclient.McpClientApplication : Started McpClientApplicationin0.842seconds (process running for1.083)
>>> QUESTION: 杭州今天天气如何?
>>> ASSISTANT: 杭州当前的天气信息如下:- 温度:24.4°C- 风速:3.4km/h
请注意,这些信息是基于当前时间的实时数据。

STDIO

server

与SSE模式相比,服务端只需要修改配置文件即可。由于是通过标准输入输出的方式提供服务,服务端不需要开放端口,因此注释掉端口号。同时需要修改web应用类型为none,禁掉banner输出(原因后面会讲)。配置MCP server的类型为stdio,服务名称和版本号,以供客户端发现。

#server.port=8080
spring.main.web-application-type=nonespring.main.banner-mode=off
spring.ai.mcp.server.stdio=truespring.ai.mcp.server.name=my-weather-serverspring.ai.mcp.server.version=0.0.1

修改完之后通过maven package打包成jar文件。

client

客户端增加mcp-servers-config.json配置路径,启用toolcallback,注释掉sse连接。

spring.ai.openai.api-key=sk-XXXXXXspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1spring.ai.openai.chat.options.model=qwen-max
spring.ai.mcp.client.stdio.servers-configuration=classpath:/mcp-servers-config.jsonspring.ai.mcp.client.toolcallback.enabled=true#spring.ai.mcp.client.sse.connections.server1.url=http://localhost: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"]}}}
<?xml version="1.0"encoding="UTF-8"?><projectxmlns="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.main.web-application-type=none

1. Spring Boot 自动配置与 WebApplicationType

Spring Boot 在启动时会根据类路径自动检测应用类型(ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">WebApplicationType),并加载对应的自动配置:

  • 若检测到 WebFlux 相关依赖,则创建ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">ReactiveWebApplicationContext,并尝试注册ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">ReactiveWebServerFactory
  • 若检测到 Servlet(Spring MVC)相关依赖,则创建:ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">
    ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">ServletWebServerApplicationContext,并尝试注册ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">ServletWebServerFactory
  • 若未检测到任何 Web 依赖,或显式设置为ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">NONE,则不会初始化任何内嵌 Web 容器。

2. 缺少对应的 Starter 依赖

  • 缺少 WebFlux Starter:若项目未引入ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">spring-boot-starter-webflux,则不会创建ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">ReactiveWebServerFactory,导致启动ingFang SC", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif;letter-spacing: 0.034em;font-style: normal;font-weight: normal;">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才会激活相应的自动配置并注册ToolCallbackProviderBean,以便在 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:00ERROR69857---[onPool-worker-1]i.m.c.t.HttpClientSseClientTransport:Errorsendingmessage:500

Java端报错:

INFO:  127.0.0.1:55085-"GET /sse HTTP/1.1"200OKWARNING: Unsupported upgrade request.INFO:  127.0.0.1:55087-"POST /messages/?session_id=5b92a6377fcb4b3fa9f051b43d0379b5 HTTP/1.1"500Internal Server ErrorERROR:  ExceptioninASGI applicationTraceback(most recent call last): File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line409,inrun_asgi  result =awaitapp( # 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", line60,in__call__ returnawaitself.app(scope, receive, send)     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/applications.py", line112,in__call__ awaitself.middleware_stack(scope, receive, send) File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line187,in__call__ raiseexc File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line165,in__call__ awaitself.app(scope, receive, _send) File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line62,in__call__ awaitwrap_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", line53,inwrapped_app raiseexc File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line42,inwrapped_app awaitapp(scope, receive, sender) File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line714,in__call__ awaitself.middleware_stack(scope, receive, send) File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line734,inapp awaitroute.handle(scope, receive, send) File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line460,inhandle awaitself.app(scope, receive, send) File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/mcp/server/sse.py", line159,inhandle_post_message  json =awaitrequest.json()     ^^^^^^^^^^^^^^^^^^^^ File"/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/requests.py", line248,injson  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", line346,inloads 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", line345,indecode  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", line363,inraw_decode raiseJSONDecodeError("Expecting value", s, err.value)fromNonejson.decoder.JSONDecodeError: Expecting value: line1column1(char0)

原因

追源码发现Java MCP client在初始化阶段发送POST请求(见第二段代码的28行)body为空,导致python MCP server端json反序列化(第一段代码27行)空字符串b''失败。暂时无法解决。如果想要用Spring AI,就需要重写mcp server。

asyncdefhandle_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") ifsession_id_paramisNone: logger.warning("Received request without session_id") response = Response("session_id is required", status_code=400) returnawaitresponse(scope, receive, send)
try: session_id = UUID(hex=session_id_param) logger.debug(f"Parsed session ID:{session_id}") exceptValueError: logger.warning(f"Received invalid session ID:{session_id_param}") response = Response("Invalid session ID", status_code=400) returnawaitresponse(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) returnawaitresponse(scope, receive, send)
json =awaitrequest.json() logger.debug(f"Received JSON:{json}")
try: message = types.JSONRPCMessage.model_validate(json) logger.debug(f"Validated client message:{message}") exceptValidationErroraserr: logger.error(f"Failed to parse message:{err}") response = Response("Could not parse message", status_code=400) awaitresponse(scope, receive, send) awaitwriter.send(err) return
logger.debug(f"Sending message to writer:{message}") response = Response("Accepted", status_code=202) awaitresponse(scope, receive, send) awaitwriter.send(message)
@OverridepublicMono<Void> sendMessage(JSONRPCMessage message){ if(isClosing) {   returnMono.empty();  }
try{ if(!closeLatch.await(10, TimeUnit.SECONDS)) { returnMono.error(new McpError("Failed to wait for the message endpoint")); } } catch(InterruptedException e) { returnMono.error(new McpError("Failed to wait for the message endpoint")); }
String endpoint = messageEndpoint.get(); if(endpoint ==null) { returnMono.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();
returnMono.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) { returnMono.error(new RuntimeException("Failed to serialize message", e)); } returnMono.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;
importcom.openai.client.OpenAIClient;importcom.openai.client.okhttp.OpenAIOkHttpClient;importcom.openai.core.JsonValue;importcom.openai.models.FunctionDefinition;importcom.openai.models.FunctionParameters;importcom.openai.models.chat.completions.*;importio.modelcontextprotocol.client.McpClient;importio.modelcontextprotocol.client.McpSyncClient;importio.modelcontextprotocol.client.transport.HttpClientSseClientTransport;importio.modelcontextprotocol.spec.McpClientTransport;importio.modelcontextprotocol.spec.McpSchema;importjakarta.annotation.PostConstruct;importjakarta.annotation.PreDestroy;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Service;importjava.util.ArrayList;importjava.util.Arrays;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importjava.util.Objects;importjava.util.stream.Collectors;importstaticcom.openai.models.chat.completions.ChatCompletion.Choice.FinishReason.TOOL_CALLS;/*** @author clong*/@ServicepublicclassMyMCPClient{privatestaticfinalLogger logger = LoggerFactory.getLogger(MyMCPClient.class); @Value("${spring.ai.openai.base-url}")privateString baseUrl; @Value("${spring.ai.openai.api-key}")privateString apiKey; @Value("${spring.ai.openai.chat.options.model}")privateString model;// Tool 名称到 MCP Client 的映射privatefinalMap<String, McpSyncClient> toolToClient =newHashMap<>(); @Value("${mcp.servers}") // e.g. tool1=http://url1,tool2=http://url2,...privateString toolServerMapping;privateOpenAIClient openaiClient;privatefinalList<McpSchema.Tool> allTools =newArrayList<>(); @PostConstructpublicvoidinit(){// 解析配置并初始化各 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); } @PreDestroypublicvoidshutdown(){// 如果有必要,优雅关闭 MCP 客户端 toolToClient.values().forEach((client) -> {try{ client.close(); logger.info("Closed MCP client for {}", client); }catch(Exception e) { logger.warn("Error closing MCP client for {}: {}", client, e.getMessage()); } }); }/** * 处理一次用户查询:注入所有工具定义 -> 发首轮请求 -> 若触发 function_call 则执行 -> * 再次发请求获取最终回复 */publicStringprocessQuery(String query){try{ List<ChatCompletionTool> chatTools = allTools.stream() .map(t -> ChatCompletionTool.builder() .function(FunctionDefinition.builder() .name(t.name()) .description(t.description()) .parameters(FunctionParameters.builder() .putAdditionalProperty("type", JsonValue.from(t.inputSchema().type())) .putAdditionalProperty("properties", JsonValue.from(t.inputSchema().properties())) .putAdditionalProperty("required", JsonValue.from(t.inputSchema().required())) .putAdditionalProperty("additionalProperties", JsonValue.from(t.inputSchema().additionalProperties())) .build()) .build()) .build()) .toList();// 2. 构建对话参数 ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder() .model(model) .maxCompletionTokens(1000) .tools(chatTools) .addUserMessage(query);// 3. 首次调用(可能包含 function_call) ChatCompletion initial = openaiClient.chat() .completions() .create(builder.build());// 4. 处理模型回复 List<ChatCompletion.Choice> choices = initial.choices();if(choices.isEmpty()) {return"[Error] empty response from model"; } ChatCompletion.Choice first = choices.get(0);// 如果模型触发了 function_callwhile(first.finishReason().equals(TOOL_CALLS)) { ChatCompletionMessage msg = first.message();// 如果同时触发了多个工具调用,toolCalls() 会返回一个列表 List<ChatCompletionMessageToolCall> calls = msg .toolCalls() // Optional<List<...>>// 若无调用则空列表 .orElse(List.of()); builder.addMessage(msg);for(ChatCompletionMessageToolCall call : calls) { ChatCompletionMessageToolCall.Function fn = call.function();// 执行 MCP 工具 String toolResult = callMcpTool(fn.name(), fn.arguments()); logger.info("Tool {} returned: {}", fn.name(), toolResult);// 将 function_call 与工具执行结果注入上下文 builder.addMessage(ChatCompletionToolMessageParam.builder() .toolCallId(Objects.requireNonNull(msg.toolCalls().orElse(null)).get(0).id()) .content(toolResult) .build()); }// 5. 二次调用,拿最终回复 ChatCompletion followup = openaiClient.chat() .completions() .create(builder.build()); first = followup.choices().get(0); }// 若未触发函数调用,直接返回文本returnfirst.message().content().orElse("无返回文本"); }catch(Exception e) { logger.error("Unexpected error during processQuery", e);return"[Error] "+ e.getMessage(); } }/** * 调用 MCP Server 上的工具并返回结果文本 */privateStringcallMcpTool(String name, String arguments){try{ McpSchema.CallToolRequest req =newMcpSchema.CallToolRequest(name, arguments);returntoolToClient.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;
importcom.openai.client.OpenAIClient;importcom.openai.client.okhttp.OpenAIOkHttpClient;importcom.openai.core.JsonValue;importcom.openai.models.FunctionDefinition;importcom.openai.models.FunctionParameters;importcom.openai.models.chat.completions.*;importio.modelcontextprotocol.client.McpClient;importio.modelcontextprotocol.client.McpSyncClient;importio.modelcontextprotocol.client.transport.HttpClientSseClientTransport;importio.modelcontextprotocol.spec.McpClientTransport;importio.modelcontextprotocol.spec.McpSchema;importjakarta.annotation.PostConstruct;importjakarta.annotation.PreDestroy;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Value;importorg.springframework.stereotype.Service;importjava.util.ArrayList;importjava.util.Arrays;importjava.util.HashMap;importjava.util.List;importjava.util.Map;importjava.util.Objects;importjava.util.stream.Collectors;importstatic com.openai.models.chat.completions.ChatCompletion.Choice.FinishReason.TOOL_CALLS;/***@authorclong*/@ServicepublicclassMyMCPClient { privatestaticfinalLoggerlogger =LoggerFactory.getLogger(MyMCPClient.class); @Value("${spring.ai.openai.base-url}") privateStringbaseUrl; @Value("${spring.ai.openai.api-key}") privateStringapiKey; @Value("${spring.ai.openai.chat.options.model}") privateStringmodel; // Tool 名称到 MCP Client 的映射 privatefinalMap<String,McpSyncClient> toolToClient =newHashMap<>(); @Value("${mcp.servers}") // e.g. tool1=http://url1,tool2=http://url2,... privateStringtoolServerMapping; privateOpenAIClientopenaiClient; privatefinalList<McpSchema.Tool> allTools =newArrayList<>(); @PostConstruct publicvoidinit(){ // 解析配置并初始化各 MCP Client Arrays.stream(toolServerMapping.split(",")) .map(entry -> entry.split("=")) .forEach(pair -> { Stringurl = pair[1]; McpClientTransporttransport = HttpClientSseClientTransport.builder(url).build(); McpSyncClientclient =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(Exceptione) { logger.warn("Error closing MCP client for {}: {}", client, e.getMessage()); } }); } /** * 处理一次用户查询:注入所有工具定义 -> 发首轮请求 -> 若触发 function_call 则执行 -> * 再次发请求获取最终回复 */ publicStringprocessQuery(Stringquery){ 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.Builderbuilder =ChatCompletionCreateParams.builder() .model(model) .maxCompletionTokens(1000) .tools(chatTools) .addUserMessage(query); // 3. 首次调用(可能包含 function_call) ChatCompletioninitial = openaiClient.chat() .completions() .create(builder.build()); // 4. 处理模型回复 List<ChatCompletion.Choice> choices = initial.choices(); if(choices.isEmpty()) { return"[Error] empty response from model"; } ChatCompletion.Choicefirst = choices.get(0); // 如果模型触发了 function_call while(first.finishReason().equals(TOOL_CALLS)) { ChatCompletionMessagemsg = first.message(); // 如果同时触发了多个工具调用,toolCalls() 会返回一个列表 List<ChatCompletionMessageToolCall> calls = msg .toolCalls() // Optional<List<...>> // 若无调用则空列表 .orElse(List.of()); builder.addMessage(msg); for(ChatCompletionMessageToolCallcall : calls) { ChatCompletionMessageToolCall.Functionfn = call.function(); // 执行 MCP 工具 StringtoolResult =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. 二次调用,拿最终回复 ChatCompletionfollowup = openaiClient.chat() .completions() .create(builder.build()); first = followup.choices().get(0); } // 若未触发函数调用,直接返回文本 returnfirst.message().content().orElse("无返回文本"); }catch(Exceptione) { logger.error("Unexpected error during processQuery", e); return"[Error] "+ e.getMessage(); } } /** * 调用 MCP Server 上的工具并返回结果文本 */ privateStringcallMcpTool(Stringname,Stringarguments){ try{ McpSchema.CallToolRequestreq =newMcpSchema.CallToolRequest(name,arguments); returntoolToClient.get(name).callTool(req) .content() .stream() .map(Object::toString) .collect(Collectors.joining("\n")); }catch(Exceptione) { logger.error("Failed to call MCP tool {}: {}", name, e.getMessage()); return"[Tool Error] "+ e.getMessage(); } }}
<?xml version="1.0"encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.alibaba.damo</groupId> <artifactId>mcp-client</artifactId> <version>0.0.1-SNAPSHOT</version> <name>mcp-client</name> <description>mcp-client</description> <properties>   <java.version>17</java.version>   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>   <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>   <spring-boot.version>3.4.0</spring-boot.version>   <mcp.version>0.9.0</mcp.version> </properties> <dependencies>   <dependency>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter</artifactId>   </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>com.openai</groupId> <artifactId>openai-java</artifactId> <version>1.6.0</version> </dependency>
</dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-bom</artifactId> <version>1.0.0-M7</version> <type>pom</type> <scope>import</scope> </dependency> <!-- MCP BOM 统一版本 --> <dependency> <groupId>io.modelcontextprotocol.sdk</groupId> <artifactId>mcp-bom</artifactId> <version>${mcp.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>17</source> <target>17</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <mainClass>com.alibaba.damo.mcpclient.McpClientApplication</mainClass> <skip>true</skip> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
</project>
spring.ai.openai.api-key=sk-xxxxspring.ai.openai.base-url=https://dashscope.aliyuncs.com/compatible-mode/v1spring.ai.openai.chat.options.model=qwen-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):把请求发给对应的MCPServer,同步拿回执行结果文本3.将模型的function_call消息和工具执行结果消息依次注入对话上下文(builder.addMessage(...))4.用更新后的上下文,再次调用openaiClient.chat().completions().create(...),获取新的`first`}
    • 通过这个循环,模型能够“问了就答、答了再问、再答”,直到它不再触发function_call,而是以纯文本的形式给出最终响应;
    • 最终,取出first.message().content()作为完整回答返回。

核心优势

  • 动态工具调用:让 LLM 在对话中主动“点”外部工具,获取实时、可信的数据;
  • 对话式编排:多轮注入上下文,不丢失模型的思考链路,保证回答连贯;
  • 解耦清晰:把“模型对话”和“工具执行”分离,用循环机制优雅衔接。

测试

启动springboot服务,通过controller接口post请求测试。

@RestControllerpublicclassController { @Autowired privateMyMCPClientmyMCPClient;
@PostMapping("/client") publicStringclient(@RequestParam("query")Stringquery){ returnmyMCPClient.processQuery(query); }}

client端日志:

2025-05-07T17:35:42.974+08:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Toolget_scene_inforeturned: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:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Toolget_hyper3d_statusreturned:TextContent[audience=null,priority=null,text=Hyper3DRodinintegrationiscurrentlydisabled.Toenableit:1.Inthe3DViewport,findtheBlenderMCPpanelinthesidebar(pressNifhidden)2.Checkthe'UseHyper3DRodin3Dmodelgeneration'checkbox3.RestarttheconnectiontoClaude]2025-05-07T17:35:50.276+08:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Tooldelete_objectreturned:TextContent[audience=null,priority=null,text=Deletedobject:WaterCup]2025-05-07T17:35:54.286+08:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Tooldelete_objectreturned:TextContent[audience=null,priority=null,text=Deletedobject:Camera]2025-05-07T17:35:58.516+08:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Tooldelete_objectreturned:TextContent[audience=null,priority=null,text=Deletedobject:MainLight]2025-05-07T17:36:23.889+08:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Toolexecute_blender_codereturned:TextContent[audience=null,priority=null,text=Codeexecutedsuccessfully:]2025-05-07T17:36:27.330+08:00INFO3127---[nio-8080-exec-4]c.a.damo.mcpclient.client.MyMCPClient:Toolsave_scenereturned:TextContent[audience=null,priority=null,text=Scenesavedto/Users/clong/Pictures/pig.blend]

server端部分核心日志

2025-05-0717:36:23,716-BlenderMCPServer-INFO-Sendingcommand:execute_codewithparams:{'code':'importbpy\nimportmath\n\n#Createthepig\'sbody\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#Createthepig\'shead\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#Createthepig\'ssnout\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#Createthenostrils\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#Createtheeyes\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#Createtheears\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#Createthelegs\ndefcreate_leg(name,x,y):\nbpy.ops.mesh.primitive_cylinder_add(radius=0.2,depth=0.7,location=(x,y,0.3))\nleg=bpy.context.active_object\nleg.name=name\nreturnleg\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#Createthetail\nbpy.ops.curve.primitive_bezier_curve_add(location=(-1.5,0,0.8))\ntail=bpy.context.active_object\ntail.name="Pig_Tail"\n\n#Modifythetailcurve\ntail.data.bevel_depth=0.05\ntail.data.bevel_resolution=4\ntail.data.fill_mode=\'FULL\'\n\n#Setthecurvepoints\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#Createthemainpigmaterial\npig_mat=bpy.data.materials.new(name="Pig_Material")\npig_mat.diffuse_color=(0.9,0.6,0.6,1.0)\n\n#Createtheeyematerial\neye_mat=bpy.data.materials.new(name="Eye_Material")\neye_mat.diffuse_color=(0.0,0.0,0.0,1.0)\n\n#Createthenostrilmaterial\nnostril_mat=bpy.data.materials.new(name="Nostril_Material")\nnostril_mat.diffuse_color=(0.2,0.0,0.0,1.0)\n\n#Applymaterials\nforobjinbpy.data.objects:\nifobj.name.startswith("Pig_")andnotobj.name.startswith("Pig_Eye")andnotobj.name.startswith("Pig_Nostril"):\nifobj.data.materials:\nobj.data.materials[0]=pig_mat\nelse:\nobj.data.materials.append(pig_mat)\n\nifobj.name.startswith("Pig_Eye"):\nifobj.data.materials:\nobj.data.materials[0]=eye_mat\nelse:\nobj.data.materials.append(eye_mat)\n\nifobj.name.startswith("Pig_Nostril"):\nifobj.data.materials:\nobj.data.materials[0]=nostril_mat\nelse:\nobj.data.materials.append(nostril_mat)\n\n#Createanewcollectionforthepig\npig_collection=bpy.data.collections.new("Pig")\nbpy.context.scene.collection.children.link(pig_collection)\n\n#Addallpigobjectstothecollection\nforobjinbpy.data.objects:\nifobj.name.startswith("Pig_"):\n#Firstremovefromcurrentcollection\nforcollinobj.users_collection:\ncoll.objects.unlink(obj)\n#Thenaddtothepigcollection\npig_collection.objects.link(obj)\n\n#Addanewcamera\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#Addalight\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-0717:36:23,718-BlenderMCPServer-INFO-Commandsent,waitingforresponse...2025-05-0717:36:23,884-BlenderMCPServer-INFO-Receivedcompleteresponse(51bytes)2025-05-0717:36:23,884-BlenderMCPServer-INFO-Received51bytesofdata2025-05-0717:36:23,884-BlenderMCPServer-INFO-Responseparsed,status:success2025-05-0717:36:23,884-BlenderMCPServer-INFO-Responseresult:{'executed':True}INFO:127.0.0.1:51875-"POST/messages/?session_id=fa4b43638d574203b05d40cd283c78cfHTTP/1.1"202Accepted2025-05-0717:36:27,020-mcp.server.lowlevel.server-INFO-ProcessingrequestoftypeCallToolRequest2025-05-0717:36:27,020-BlenderMCPServer-INFO-Sendingcommand:get_polyhaven_statuswithparams:None2025-05-0717:36:27,021-BlenderMCPServer-INFO-Commandsent,waitingforresponse...2025-05-0717:36:27,121-BlenderMCPServer-INFO-Receivedcompleteresponse(115bytes)2025-05-0717:36:27,121-BlenderMCPServer-INFO-Received115bytesofdata2025-05-0717:36:27,121-BlenderMCPServer-INFO-Responseparsed,status:success2025-05-0717:36:27,121-BlenderMCPServer-INFO-Responseresult:{'enabled':True,'message':'PolyHavenintegrationisenabledandreadytouse.'}2025-05-0717:36:27,121-BlenderMCPServer-INFO-Sendingcommand:save_scenewithparams:{'filepath':'/Users/clong/Pictures/pig.blend'}2025-05-0717:36:27,121-BlenderMCPServer-INFO-Commandsent,waitingforresponse...2025-05-0717:36:27,326-BlenderMCPServer-INFO-Receivedcompleteresponse(80bytes)2025-05-0717:36:27,327-BlenderMCPServer-INFO-Received80bytesofdata2025-05-0717:36:27,327-BlenderMCPServer-INFO-Responseparsed,status:success2025-05-0717:36:27,327-BlenderMCPServer-INFO-Responseresult:{'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 应用开发的常态。期待更多开发者加入这一探索,共同构建开放、兼容、可扩展的智能系统。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

链载AI是专业的生成式人工智能教程平台。提供Stable Diffusion、Midjourney AI绘画教程,Suno AI音乐生成指南,以及Runway、Pika等AI视频制作与动画生成实战案例。从提示词编写到参数调整,手把手助您从入门到精通。
  • 官方手机版

  • 微信公众号

  • 商务合作

  • Powered by Discuz! X3.5 | Copyright © 2025-2025. | 链载Ai
  • 桂ICP备2024021734号 | 营业执照 | |广西笔趣文化传媒有限公司|| QQ