支持私有化部署
AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


MCP Java 开发指南

发布日期:2025-06-04 18:15:56 浏览次数: 1627 作者:阿里云开发者
推荐语

Java开发者如何高效集成MCP服务器,打破Python开发局限。

核心内容:
1. MCP Java开发背景与必要性
2. MCP协议概述及其核心特性
3. Java集成MCP的架构与传输实现

杨芳贤
53A创始人/腾讯云(TVP)最具价值专家

背景

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

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. 典型使用场景

SSE

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

STDIO

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

4. 优缺点对比

总结:

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

SSE

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 */@ServicepublicclassOpenMeteoService {
    privatefinal WebClient webClient;
    publicOpenMeteoService(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;
@SpringBootApplicationpublicclassMcpServerApplication {
    publicstaticvoidmain(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();    }
}

配置文件:

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"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>com.alibaba.damo</groupId>    <artifactId>mcp-server</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>mcp-server</name>    <description>mcp-server</description>    <properties>        <java.version>17</java.version>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>        <spring-boot.version>3.0.2</spring-boot.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter</artifactId>        </dependency>
        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>
        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>            <version>1.0.0-M7</version>        </dependency>
        <dependency>            <groupId>io.netty</groupId>            <artifactId>netty-resolver-dns-native-macos</artifactId>            <version>4.1.79.Final</version>            <scope>runtime</scope>            <classifier>osx-aarch_64</classifier>        </dependency>    </dependencies>    <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-dependencies</artifactId>                <version>${spring-boot.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement>
    <build>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-compiler-plugin</artifactId>                <version>3.8.1</version>                <configuration>                    <source>17</source>                    <target>17</target>                    <encoding>UTF-8</encoding>                </configuration>            </plugin>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <version>${spring-boot.version}</version>                <configuration>                    <mainClass>com.alibaba.damo.mcpserver.McpServerApplication</mainClass>                    <skip>true</skip>                </configuration>                <executions>                    <execution>                        <id>repackage</id>                        <goals>                            <goal>repackage</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build>
</project>

启动日志:

2025-04-29T20:09:07.153+08:00  INFO 51478 --- [           main] c.a.damo.mcpserver.McpServerApplication  : Starting McpServerApplication using Java 17.0.15 with PID 51478 (/Users/clong/IdeaProjects/mcp-server/target/classes started by clong in /Users/clong/IdeaProjects/mcp-server)2025-04-29T20:09:07.154+08:00  INFO 51478 --- [           main] c.a.damo.mcpserver.McpServerApplication  : No active profile set, falling back to 1default profile: "default"2025-04-29T20:09:07.561+08:00  INFO 51478 --- [           main] o.s.a.m.s.a.McpServerAutoConfiguration   : Registered tools: 2, notification: true2025-04-29T20:09:07.609+08:00  INFO 51478 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port 80802025-04-29T20:09:07.612+08:00  INFO 51478 --- [           main] c.a.damo.mcpserver.McpServerApplication  : Started McpServerApplication in 0.572 seconds (process running for0.765)2025-04-29T20:10:18.812+08:00  INFO 51478 --- [ctor-http-nio-3] i.m.server.McpAsyncServer                : Client initialize request - Protocol: 2024-11-05, Capabilities: ClientCapabilities[experimental=null, roots=null, sampling=null], Info: Implementation[name=spring-ai-mcp-client - server1, version=1.0.0]

client

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

package com.alibaba.damo.mcpclient;
import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.tool.ToolCallbackProvider;import org.springframework.boot.CommandLineRunner;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;import org.springframework.context.annotation.Bean;
import java.util.Arrays;
@SpringBootApplicationpublicclassMcpClientApplication {
    publicstaticvoidmain(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

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"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>com.alibaba.damo</groupId>    <artifactId>mcp-client</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>mcp-client</name>    <description>mcp-client</description>    <properties>        <java.version>17</java.version>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>        <spring-boot.version>3.4.0</spring-boot.version>        <mcp.version>0.9.0</mcp.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter</artifactId>        </dependency>
        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>
        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-starter-mcp-client</artifactId>            <version>1.0.0-M7</version>        </dependency>        <!-- openai model -->        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-starter-model-openai</artifactId>            <version>1.0.0-M7</version>        </dependency>        <dependency>            <groupId>io.modelcontextprotocol.sdk</groupId>            <artifactId>mcp</artifactId>            <version>0.9.0</version>        </dependency>
    </dependencies>    <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-dependencies</artifactId>                <version>${spring-boot.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <dependency>                <groupId>org.springframework.ai</groupId>                <artifactId>spring-ai-bom</artifactId>                <version>1.0.0-M7</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <!-- MCP BOM 统一版本 -->            <dependency>                <groupId>io.modelcontextprotocol.sdk</groupId>                <artifactId>mcp-bom</artifactId>                <version>${mcp.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement>
    <build>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-compiler-plugin</artifactId>                <version>3.8.1</version>                <configuration>                    <source>17</source>                    <target>17</target>                    <encoding>UTF-8</encoding>                </configuration>            </plugin>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <version>${spring-boot.version}</version>                <configuration>                    <mainClass>com.alibaba.damo.mcpclient.McpClientApplication</mainClass>                    <skip>true</skip>                </configuration>                <executions>                    <execution>                        <id>repackage</id>                        <goals>                            <goal>repackage</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build>
</project>

客户端调用日志如下:

2025-04-29T20:10:18.302+08:00  INFO 51843 --- [           main] c.a.damo.mcpclient.McpClientApplication  : Starting McpClientApplication using Java 17.0.15 with PID 51843 (/Users/clong/IdeaProjects/mcp-client/target/classes started by clong in /Users/clong/IdeaProjects/mcp-client)2025-04-29T20:10:18.303+08:00  INFO 51843 --- [           main] c.a.damo.mcpclient.McpClientApplication  : No active profile set, falling back to 1default profile: "default"2025-04-29T20:10:18.846+08:00  INFO 51843 --- [ient-1-Worker-0] i.m.client.McpAsyncClient                : Server response with Protocol: 2024-11-05, Capabilities: ServerCapabilities[experimental=null, logging=LoggingCapabilities[], prompts=null, resources=null, tools=ToolCapabilities[listChanged=true]], Info: Implementation[name=my-weather-server, version=0.0.1and Instructions null2025-04-29T20:10:19.018+08:00  INFO 51843 --- [           main] c.a.damo.mcpclient.McpClientApplication  : Started McpClientApplication in 0.842 seconds (process running for1.083)
>>> QUESTION: 杭州今天天气如何?
>>> ASSISTANT: 杭州当前的天气信息如下:- 温度:24.4°C- 风速:3.4 km/h
请注意,这些信息是基于当前时间的实时数据。

STDIO

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"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>com.alibaba.damo</groupId>    <artifactId>mcp-client</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>mcp-client</name>    <description>mcp-client</description>    <properties>        <java.version>17</java.version>        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>        <spring-boot.version>3.4.0</spring-boot.version>        <mcp.version>0.9.0</mcp.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter</artifactId>        </dependency>
        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>
        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-starter-mcp-client</artifactId>            <version>1.0.0-M7</version>        </dependency>        <!-- openai model -->        <dependency>            <groupId>org.springframework.ai</groupId>            <artifactId>spring-ai-starter-model-openai</artifactId>            <version>1.0.0-M7</version>        </dependency><!--        <dependency>--><!--            <groupId>io.modelcontextprotocol.sdk</groupId>--><!--            <artifactId>mcp</artifactId>--><!--            <version>0.9.0</version>--><!--        </dependency>-->
    </dependencies>    <dependencyManagement>        <dependencies>            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-dependencies</artifactId>                <version>${spring-boot.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <dependency>                <groupId>org.springframework.ai</groupId>                <artifactId>spring-ai-bom</artifactId>                <version>1.0.0-M7</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <!-- MCP BOM 统一版本 -->            <dependency>                <groupId>io.modelcontextprotocol.sdk</groupId>                <artifactId>mcp-bom</artifactId>                <version>${mcp.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>        </dependencies>    </dependencyManagement>
    <build>        <plugins>            <plugin>                <groupId>org.apache.maven.plugins</groupId>                <artifactId>maven-compiler-plugin</artifactId>                <version>3.8.1</version>                <configuration>                    <source>17</source>                    <target>17</target>                    <encoding>UTF-8</encoding>                </configuration>            </plugin>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <version>${spring-boot.version}</version>                <configuration>                    <mainClass>com.alibaba.damo.mcpclient.McpClientApplication</mainClass>                    <skip>true</skip>                </configuration>                <executions>                    <execution>                        <id>repackage</id>                        <goals>                            <goal>repackage</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build>
</project>

日志同上,不再打印。

重要配置项解析

spring.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 OKWARNING:  Unsupported upgrade request.INFO:     127.0.0.1:55087 - "POST /messages/?session_id=5b92a6377fcb4b3fa9f051b43d0379b5 HTTP/1.1"500 Internal Server ErrorERROR:    Exception in ASGI applicationTraceback(most recent call last):  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/uvicorn/protocols/http/httptools_impl.py", line 409in 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 60in __call__    return await self.app(scope, receive, send)           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/applications.py", line 112in __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 187in __call__    raise exc  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/errors.py", line 165in __call__    await self.app(scope, receive, _send)  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/middleware/exceptions.py", line 62in __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 53in wrapped_app    raise exc  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/_exception_handler.py", line 42in wrapped_app    await app(scope, receive, sender)  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 714in __call__    await self.middleware_stack(scope, receive, send)  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 734in app    await route.handle(scope, receive, send)  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/routing.py", line 460in handle    await self.app(scope, receive, send)  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/mcp/server/sse.py", line 159in handle_post_message    json = await request.json()           ^^^^^^^^^^^^^^^^^^^^  File "/Users/clong/PycharmProjects/blender-mcp/.venv/lib/python3.13/site-packages/starlette/requests.py", line 248in 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 346in 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 345in 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 363in raw_decode    raise JSONDecodeError("Expecting value", s, err.value) from Nonejson.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char0)

原因

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

async def handle_post_message(    self, scope: Scope, receive: Receive, send: Send) -> None:    logger.debug("Handling POST message")    request = Request(scope, receive)
    session_id_param = request.query_params.get("session_id")    if session_id_param is None:        logger.warning("Received request without session_id")        response = Response("session_id is required", status_code=400)        return await response(scope, receive, send)
    try:        session_id = UUID(hex=session_id_param)        logger.debug(f"Parsed session ID: {session_id}")    except ValueError:        logger.warning(f"Received invalid session ID: {session_id_param}")        response = Response("Invalid session ID", status_code=400)        return await response(scope, receive, send)
    writer = self._read_stream_writers.get(session_id)    ifnot writer:        logger.warning(f"Could not find session for ID: {session_id}")        response = Response("Could not find session", status_code=404)        return await response(scope, receive, send)
    json = await request.json()    logger.debug(f"Received JSON: {json}")
    try:        message = types.JSONRPCMessage.model_validate(json)        logger.debug(f"Validated client message: {message}")    except ValidationError as err:        logger.error(f"Failed to parse message: {err}")        response = Response("Could not parse message", status_code=400)        await response(scope, receive, send)        await writer.send(err)        return
    logger.debug(f"Sending message to writer: {message}")    response = Response("Accepted", status_code=202)    await response(scope, receive, send)    await writer.send(message)
@Overridepublic Mono<Void> sendMessage(JSONRPCMessage message){    if (isClosing) {        return Mono.empty();    }
    try {        if (!closeLatch.await(10, TimeUnit.SECONDS)) {            return Mono.error(new McpError("Failed to wait for the message endpoint"));        }    }    catch (InterruptedException e) {        return Mono.error(new McpError("Failed to wait for the message endpoint"));    }
    String endpoint = messageEndpoint.get();    if (endpoint == null) {        return Mono.error(new McpError("No message endpoint available"));    }
    try {        String jsonText = this.objectMapper.writeValueAsString(message);        HttpRequest request = this.requestBuilder.uri(URI.create(this.baseUri + endpoint))            .POST(HttpRequest.BodyPublishers.ofString(jsonText))            .build();
        return Mono.fromFuture(                httpClient.sendAsync(request, HttpResponse.BodyHandlers.discarding()).thenAccept(response -> {                    if (response.statusCode() != 200 && response.statusCode() != 201 && response.statusCode() != 202                            && response.statusCode() != 206) {                        logger.error("Error sending message: {}", response.statusCode());                    }                }));    }    catch (IOException e) {        if (!isClosing) {            return Mono.error(new RuntimeException("Failed to serialize message", e));        }        return Mono.empty();    }}

重写server的方式比较繁琐且不适用,对于目前绝大部分的MCP server都是python开发的现状下,尽量不动mcp server端的代码最好,因此,我尝试从client端着手,抛弃spring AI的包装,尝试使用原生的mcp java sdk+openai java sdk来实现一个类似Claude desktop这样的支持MCP调用的client。

3.2 mcp java sdk

代码实现

package com.alibaba.damo.mcpclient.client;
import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.core.JsonValue;import com.openai.models.FunctionDefinition;import com.openai.models.FunctionParameters;import com.openai.models.chat.completions.*;import io.modelcontextprotocol.client.McpClient;import io.modelcontextprotocol.client.McpSyncClient;import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;import io.modelcontextprotocol.spec.McpClientTransport;import io.modelcontextprotocol.spec.McpSchema;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.stream.Collectors;importstatic com.openai.models.chat.completions.ChatCompletion.Choice.FinishReason.TOOL_CALLS;/** * @author clong */@ServicepublicclassMyMCPClient {privatestaticfinal Logger logger = LoggerFactory.getLogger(MyMCPClient.class);    @Value("${spring.ai.openai.base-url}")private String baseUrl;    @Value("${spring.ai.openai.api-key}")private String apiKey;    @Value("${spring.ai.openai.chat.options.model}")private String model;// Tool 名称到 MCP Client 的映射privatefinal Map<String, McpSyncClient> toolToClient = new HashMap<>();    @Value("${mcp.servers}")            // e.g. tool1=http://url1,tool2=http://url2,...private String toolServerMapping;private OpenAIClient openaiClient;privatefinal List<McpSchema.Tool> allTools = new ArrayList<>();    @PostConstructpublicvoidinit(){// 解析配置并初始化各 MCP 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 则执行 ->     * 再次发请求获取最终回复     */public String processQuery(String query){try {            List<ChatCompletionTool> chatTools = allTools.stream()                    .map(t -> ChatCompletionTool.builder()                            .function(FunctionDefinition.builder()                                    .name(t.name())                                    .description(t.description())                                    .parameters(FunctionParameters.builder()                                            .putAdditionalProperty("type", JsonValue.from(t.inputSchema().type()))                                            .putAdditionalProperty("properties", JsonValue.from(t.inputSchema().properties()))                                            .putAdditionalProperty("required", JsonValue.from(t.inputSchema().required()))                                            .putAdditionalProperty("additionalProperties", JsonValue.from(t.inputSchema().additionalProperties()))                                            .build())                                    .build())                            .build())                    .toList();// 2. 构建对话参数            ChatCompletionCreateParams.Builder builder = ChatCompletionCreateParams.builder()                    .model(model)                    .maxCompletionTokens(1000)                    .tools(chatTools)                    .addUserMessage(query);// 3. 首次调用(可能包含 function_call)            ChatCompletion initial = openaiClient.chat()                    .completions()                    .create(builder.build());// 4. 处理模型回复            List<ChatCompletion.Choice> choices = initial.choices();if (choices.isEmpty()) {return"[Error] empty response from model";            }            ChatCompletion.Choice first = choices.get(0);// 如果模型触发了 function_callwhile (first.finishReason().equals(TOOL_CALLS)) {                ChatCompletionMessage msg = first.message();// 如果同时触发了多个工具调用,toolCalls() 会返回一个列表                List<ChatCompletionMessageToolCall> calls = msg                        .toolCalls()                       // Optional<List<...>>// 若无调用则空列表                        .orElse(List.of());                builder.addMessage(msg);for (ChatCompletionMessageToolCall call : calls) {                    ChatCompletionMessageToolCall.Function fn = call.function();// 执行 MCP 工具                    String toolResult = callMcpTool(fn.name(), fn.arguments());                    logger.info("Tool {} returned: {}", fn.name(), toolResult);// 将 function_call 与工具执行结果注入上下文                    builder.addMessage(ChatCompletionToolMessageParam.builder()                            .toolCallId(Objects.requireNonNull(msg.toolCalls().orElse(null)).get(0).id())                            .content(toolResult)                            .build());                }// 5. 二次调用,拿最终回复                ChatCompletion followup = openaiClient.chat()                        .completions()                        .create(builder.build());                first = followup.choices().get(0);            }// 若未触发函数调用,直接返回文本return first.message().content().orElse("无返回文本");        } catch (Exception e) {            logger.error("Unexpected error during processQuery", e);return"[Error] " + e.getMessage();        }    }/**     * 调用 MCP Server 上的工具并返回结果文本     */private String callMcpTool(String name, String arguments){try {            McpSchema.CallToolRequest req = new McpSchema.CallToolRequest(name, arguments);return toolToClient.get(name).callTool(req)                    .content()                    .stream()                    .map(Object::toString)                    .collect(Collectors.joining("\n"));        } catch (Exception e) {            logger.error("Failed to call MCP tool {}: {}", name, e.getMessage());return"[Tool Error] " + e.getMessage();        }    }}


package com.alibaba.damo.mcpclient.client;
import com.openai.client.OpenAIClient;import com.openai.client.okhttp.OpenAIOkHttpClient;import com.openai.core.JsonValue;import com.openai.models.FunctionDefinition;import com.openai.models.FunctionParameters;import com.openai.models.chat.completions.*;import io.modelcontextprotocol.client.McpClient;import io.modelcontextprotocol.client.McpSyncClient;import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;import io.modelcontextprotocol.spec.McpClientTransport;import io.modelcontextprotocol.spec.McpSchema;import jakarta.annotation.PostConstruct;import jakarta.annotation.PreDestroy;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Service;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.stream.Collectors;importstatic com.openai.models.chat.completions.ChatCompletion.Choice.FinishReason.TOOL_CALLS;/** * @author clong */@ServicepublicclassMyMCPClient {    privatestaticfinal Logger logger = LoggerFactory.getLogger(MyMCPClient.class);    @Value("${spring.ai.openai.base-url}")    private String baseUrl;    @Value("${spring.ai.openai.api-key}")    private String apiKey;    @Value("${spring.ai.openai.chat.options.model}")    private String model;    // Tool 名称到 MCP Client 的映射    privatefinal Map<StringMcpSyncClient> 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-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):把请求发给对应的 MCP Server,同步拿回执行结果文本  3. 将模型的 function_call 消息和工具执行结果消息依次注入对话上下文(builder.addMessage(...))  4. 用更新后的上下文,再次调用 openaiClient.chat().completions().create(...),获取新的 `first`}
    • 通过这个循环,模型能够“问了就答、答了再问、再答”,直到它不再触发 function_call,而是以纯文本的形式给出最终响应;
    • 最终,取出 first.message().content() 作为完整回答返回。

核心优势

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

测试

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

@RestControllerpublicclassController {    @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": [        0.0,        0.0,        0.0      ]    },    {      "name": "Camera",      "type": "CAMERA",      "location": [        4.0,        -4.0,        3.0      ]    },    {      "name": "MainLight",      "type": "LIGHT",      "location": [        2.0,        -2.0,        3.0      ]    }  ],  "materials_count": 12}]2025-05-07T17:35:46.371+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool get_hyper3d_status returned: TextContent[audience=null, priority=null, text=Hyper3D Rodin integration is currently disabled. To enable it:1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden)2. Check the 'Use Hyper3D Rodin 3D model generation' checkbox3. Restart the connection to Claude]2025-05-07T17:35:50.276+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool delete_object returned: TextContent[audience=null, priority=null, text=Deleted object: WaterCup]2025-05-07T17:35:54.286+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool delete_object returned: TextContent[audience=null, priority=null, text=Deleted object: Camera]2025-05-07T17:35:58.516+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool delete_object returned: TextContent[audience=null, priority=null, text=Deleted object: MainLight]2025-05-07T17:36:23.889+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool execute_blender_code returned: TextContent[audience=null, priority=null, text=Code executed successfully: ]2025-05-07T17:36:27.330+08:00  INFO 3127 --- [nio-8080-exec-4] c.a.damo.mcpclient.client.MyMCPClient    : Tool save_scene returned: TextContent[audience=null, priority=null, text=Scene saved to /Users/clong/Pictures/pig.blend]

server端部分核心日志

2025-05-0717:36:23,716 - BlenderMCPServer - INFO - Sending command: execute_code with params: {'code': 'import bpy\nimport math\n\n# Create the pig\'s body\nbpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(0, 0, 0.8))\nbody = bpy.context.active_object\nbody.name = "Pig_Body"\nbody.scale = (1.5, 1, 0.8)\n\n# Create the pig\'s head\nbpy.ops.mesh.primitive_uv_sphere_add(radius=0.7, location=(1.5, 0, 1.2))\nhead = bpy.context.active_object\nhead.name = "Pig_Head"\n\n# Create the pig\'s snout\nbpy.ops.mesh.primitive_cylinder_add(radius=0.3, depth=0.4, location=(2.0, 0, 1.0))\nsnout = bpy.context.active_object\nsnout.name = "Pig_Snout"\nsnout.rotation_euler = (math.radians(90), 0, 0)\n\n# Create the nostrils\nbpy.ops.mesh.primitive_cylinder_add(radius=0.08, depth=0.1, location=(2.2, 0.15, 1))\nleft_nostril = bpy.context.active_object\nleft_nostril.name = "Pig_Nostril_Left"\nleft_nostril.rotation_euler = (math.radians(90), 0, 0)\n\nbpy.ops.mesh.primitive_cylinder_add(radius=0.08, depth=0.1, location=(2.2, -0.15, 1))\nright_nostril = bpy.context.active_object\nright_nostril.name = "Pig_Nostril_Right"\nright_nostril.rotation_euler = (math.radians(90), 0, 0)\n\n# Create the eyes\nbpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(1.9, 0.3, 1.5))\nleft_eye = bpy.context.active_object\nleft_eye.name = "Pig_Eye_Left"\n\nbpy.ops.mesh.primitive_uv_sphere_add(radius=0.1, location=(1.9, -0.3, 1.5))\nright_eye = bpy.context.active_object\nright_eye.name = "Pig_Eye_Right"\n\n# Create the ears\nbpy.ops.mesh.primitive_cone_add(radius1=0.3, radius2=0, depth=0.5, location=(1.3, 0.5, 1.8))\nleft_ear = bpy.context.active_object\nleft_ear.name = "Pig_Ear_Left"\nleft_ear.rotation_euler = (math.radians(-30), math.radians(-20), math.radians(20))\n\nbpy.ops.mesh.primitive_cone_add(radius1=0.3, radius2=0, depth=0.5, location=(1.3, -0.5, 1.8))\nright_ear = bpy.context.active_object\nright_ear.name = "Pig_Ear_Right"\nright_ear.rotation_euler = (math.radians(-30), math.radians(20), math.radians(-20))\n\n# Create the legs\ndef create_leg(name, x, y):\n    bpy.ops.mesh.primitive_cylinder_add(radius=0.2, depth=0.7, location=(x, y, 0.3))\n    leg = bpy.context.active_object\n    leg.name = name\n    return leg\n\nfront_left_leg = create_leg("Pig_Leg_Front_Left", 0.7, 0.5)\nfront_right_leg = create_leg("Pig_Leg_Front_Right", 0.7, -0.5)\nback_left_leg = create_leg("Pig_Leg_Back_Left", -0.7, 0.5)\nback_right_leg = create_leg("Pig_Leg_Back_Right", -0.7, -0.5)\n\n# Create the tail\nbpy.ops.curve.primitive_bezier_curve_add(location=(-1.5, 0, 0.8))\ntail = bpy.context.active_object\ntail.name = "Pig_Tail"\n\n# Modify the tail curve\ntail.data.bevel_depth = 0.05\ntail.data.bevel_resolution = 4\ntail.data.fill_mode = \'FULL\'\n\n# Set the curve points\npoints = tail.data.splines[0].bezier_points\npoints[0].co = (-1.5, 0, 0.8)\npoints[0].handle_left = (-1.5, -0.2, 0.7)\npoints[0].handle_right = (-1.5, 0.2, 0.9)\npoints[1].co = (-1.8, 0.3, 1.0)\npoints[1].handle_left = (-1.7, 0.1, 1.0)\npoints[1].handle_right = (-1.9, 0.5, 1.0)\n\n# Create the main pig material\npig_mat = bpy.data.materials.new(name="Pig_Material")\npig_mat.diffuse_color = (0.9, 0.6, 0.6, 1.0)\n\n# Create the eye material\neye_mat = bpy.data.materials.new(name="Eye_Material")\neye_mat.diffuse_color = (0.0, 0.0, 0.0, 1.0)\n\n# Create the nostril material\nnostril_mat = bpy.data.materials.new(name="Nostril_Material")\nnostril_mat.diffuse_color = (0.2, 0.0, 0.0, 1.0)\n\n# Apply materials\nfor obj in bpy.data.objects:\n    if obj.name.startswith("Pig_") andnot obj.name.startswith("Pig_Eye") andnot obj.name.startswith("Pig_Nostril"):\n        if obj.data.materials:\n            obj.data.materials[0] = pig_mat\n        else:\n            obj.data.materials.append(pig_mat)\n    \n    if obj.name.startswith("Pig_Eye"):\n        if obj.data.materials:\n            obj.data.materials[0] = eye_mat\n        else:\n            obj.data.materials.append(eye_mat)\n    \n    if obj.name.startswith("Pig_Nostril"):\n        if obj.data.materials:\n            obj.data.materials[0] = nostril_mat\n        else:\n            obj.data.materials.append(nostril_mat)\n\n# Create a new collection for the pig\npig_collection = bpy.data.collections.new("Pig")\nbpy.context.scene.collection.children.link(pig_collection)\n\n# Add all pig objects to the collection\nfor obj in bpy.data.objects:\n    if obj.name.startswith("Pig_"):\n        # First remove from current collection\n        for coll in obj.users_collection:\n            coll.objects.unlink(obj)\n        # Then add to the pig collection\n        pig_collection.objects.link(obj)\n\n# Add a new camera\nbpy.ops.object.camera_add(location=(5, -5, 3))\ncamera = bpy.context.active_object\ncamera.name = "Camera"\ncamera.rotation_euler = (math.radians(60), 0, math.radians(45))\nbpy.context.scene.camera = camera\n\n# Add a light\nbpy.ops.object.light_add(type=\'SUN\', location=(5, 5, 10))\nlight = bpy.context.active_object\nlight.name = "Sun"\nlight.rotation_euler = (math.radians(45), math.radians(45), 0)'}2025-05-07 17:36:23,718 - BlenderMCPServer - INFO - Command sent, waiting for response...2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Received complete response (51 bytes)2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Received 51 bytes of data2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Response parsed, status: success2025-05-07 17:36:23,884 - BlenderMCPServer - INFO - Response result: {'executed': True}INFO:     127.0.0.1:51875 - "POST /messages/?session_id=fa4b43638d574203b05d40cd283c78cf HTTP/1.1"202 Accepted2025-05-0717:36:27,020 - mcp.server.lowlevel.server - INFO - Processing request of type CallToolRequest2025-05-0717:36:27,020 - BlenderMCPServer - INFO - Sending command: get_polyhaven_status with params: None2025-05-0717:36:27,021 - BlenderMCPServer - INFO - Command sent, waiting for response...2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Received complete response (115 bytes)2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Received 115 bytes of data2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Response parsed, status: success2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Response result: {'enabled': True, 'message': 'PolyHaven integration is enabled and ready to use.'}2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Sending command: save_scene with params: {'filepath': '/Users/clong/Pictures/pig.blend'}2025-05-0717:36:27,121 - BlenderMCPServer - INFO - Command sent, waiting for response...2025-05-0717:36:27,326 - BlenderMCPServer - INFO - Received complete response (80 bytes)2025-05-0717:36:27,327 - BlenderMCPServer - INFO - Received 80 bytes of data2025-05-0717:36:27,327 - BlenderMCPServer - INFO - Response parsed, status: success2025-05-0717:36:27,327 - BlenderMCPServer - INFO - Response result: {'filepath': '/Users/clong/Pictures/pig.blend'}

已经保存到本地指定目录下

使用blender打开即可看到一头粉色的小猪

结语

本文系统性地探索了 Java 开发者在 MCP(Model Context Protocol)生态中的实践路径,从背景调研到技术验证,从 Spring AI 的局限性到原生 MCP SDK 的深度整合,最终实现了 Java 客户端与 Python 服务端的无缝协作。这一过程不仅验证了 MCP 协议的跨语言通用性,也为企业级 Java 应用对接主流 AI 工具链提供了可复用的解决方案。

未来展望

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 应用开发的常态。期待更多开发者加入这一探索,共同构建开放、兼容、可扩展的智能系统。

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询