Java开发者の模型召唤术(4):MCP
往期系列:
- Java开发者の模型召唤术:LangChain4j咏唱指南(一) 初识
- Java开发者の模型召唤术:LangChain4j咏唱指南(二) 相遇
- Java开发者の模型召唤术:LangChain4j咏唱指南(三) 相知
上两期博客中简单的为大家介绍了
-
langchain4j是什么、java 集成 langchain4j、集成阿里云百炼平台的各个模型以及本地安装的ollama模型的调用流程
-
langchain4jSpring-boot集成、流式输出、记忆对话、function-call、预设角色等更深层次的知识
-
langchain4j中的向量、向量存储、模型外接知识库相关的知识
放眼整个ai界,最近MCP可谓是大🔥了一波,各大模型的it爱好者纷纷投入了MCP这个活动当中,为AI大模型添加了不少的可玩性。,那么本期结合多模型集成客户端cherry studio为大家带来最近特别火的大模型MCP试用指南,目测无毒,各位观众老爷们放心食用
MCP最近这么🔥,那么什么是mcp呢?我们先一起了解一下
首先叠个buff ,之前博客中使用到的langchain4j-java-ai,据我现在的了解,目前好像是不支持mcp的
如果后续支持,欢迎小伙伴们加入一起研究。
本文中接入的是spring-ai-alibaba进行的演示
1. [MCP简介](Introduction - Model Context Protocol)
MCP 是一种开放协议,通过标准化的服务器实现,使 AI 模型能够安全地与本地和远程资源进行交互。此列表重点关注可用于生产和实验性的 MCP 服务器,这些服务器通过文件访问、数据库连接、API 集成和其他上下文服务来扩展 AI 功能。
很抽象是吧,那么我再通过 Spring-ai-alibaba 官网的一张图解释一下上述的这段话:
Spring AI MCP 为模型上下文协议提供 Java 和 Spring 框架集成。它使 Spring AI 应用程序能够通过标准化的接口与不同的数据源和工具进行交互,支持同步和异步通信模式。

Spring AI MCP 采用模块化架构,包括以下组件:
- Spring AI 应用程序:使用 Spring AI 框架构建想要通过 MCP 访问数据的生成式 AI 应用程序
- Spring MCP 客户端:MCP 协议的 Spring AI 实现,与服务器保持 1:1 连接
- MCP 服务器:轻量级程序,每个程序都通过标准化的模型上下文协议公开特定的功能
- 本地数据源:MCP 服务器可以安全访问的计算机文件、数据库和服务
- 远程服务:MCP 服务器可以通过互联网(例如,通过 API)连接到的外部系统
这好理解嘛?
好! 那我再来一张图片,并用通俗易懂的描述解释一下关于mcp:

mcp:简单的来说是一组协议,类似于上图中的接口转换器,规定了接入电脑中的接口必须是C口,并支持各种拓展。
mcp server:类比各个接入的设备,入口端连接到接口转换器上,至于连接的末端,可以是一些外部接口,各种处理流程,数据库,甚至可以是本地的文件系统。
mcp client:类比电脑再接入接口转换器后,接口转换器中的一些驱动程序,能够识别到接口转换器这个硬件,同时再有外设通过接口转换器接入时,发现外设。
各个大模型智能体:类比上图中的电脑,可以通过接口转换器以及接口转换器中的驱动程序识别到外设并使用外设。
那么总体来说,mcp提供了一组标准协议,将各个mcp 服务提供者(mcp server)通过mcp服务发现者(mcp client)发现并提供给支持或者说是接入mcp生态的各个大模型使用,但是对于服务提供者提供的一些具体的功能等,应该是多种多样的,可能包括外部接口,函数处理,数据库操作,本地文件系统等等,小伙伴们可以发挥想象哈。
近期也在github上发现了多种好玩的mcp server,链接贴这儿了,小伙伴们自行食用哈。
🤠🤠punkpeye/awesome-mcp-servers: A collection of MCP servers.
2.MCP简单代码实现
2.1 cherry Studio
首先呢,为大家介绍一款工具叭,名字叫做 cherry studio ,然后是一个支持多模型集成的对话客户端,同时还支持MCP server,为什么推荐呢?因为我只有这个,哈哈哈,当然小伙伴们也可以自己找一些类似的软件哈。
然后就是通过cherry studio 先连接上我们自己的大模型,可以看到支持多种模型的,选择自己申请过api-key的模型进行配置api-key即可,我这儿使用的是阿里云百炼平台

配置好后可以在chat 界面进行测试,发送简单的问题,如果能够回答你,说明配置成功

ps:默认好像是什么硅基流动,因为我没有这家的api-key,我直接给关了。当然有的小伙伴请忽略哈

2.2 环境声明
保持良好习惯,执行任务之前先对表,以下是我的环境信息
JDK:17 # 一定得是17或者17+才行
spring-boot-version :3.4.0 #mcp官网说的,想要接入mcp,Spring Boot 3.3.x or higher
mcp.version : 1.0.0-M6
spring-ai-alibaba.version : 1.0.0-M6.1
2.3demo实现
首先呢,更具我的理解,mcp的模式一般分为 stdio模式以及sse模式
stdio相比sse的话配置什么的稍微难那么一点,优先实现stdio模式的mpc-demo实现
⚠️整个过程中的初始化创建项目我这边不再过多赘述哈,小伙伴们参照2.2环境声明自行选择JKD,spring-boot-version 配置初始化Spring-boot 项目就行
2.3.1 stdio
2.3.1.1 stdio-server
1、引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-mcp-server-spring-boot-starter</artifactId>
<version>${mcp.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.57</version>
</dependency>
</dependencies>
除了上述依赖,我再集成的过程中出现了spring-ai 当时还未放置到中央仓库中的情况,导致拉取依赖失败的情况,如果小伙伴们也出现这种情况,请添加如下依赖
<repositories>
<!-- spring-ai-mcp-repository -->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
2、配置文件
spring:
application:
name: mcp-server-stdio
main: #spring-main下的这两个建议配置
web-application-type: none
banner-mode: off
ai:
dashscope:
api-key: sk-xxxxxxxxxxxxxxx # 阿里百炼平台的 api-key
mcp:
server:
name: my-server
version: 0.0.1
3、编写service
其实在我自己的代码demo中实现了好几个service,包括天气情况获取,空气质量获取,星座运势;这里我只展示空气质量查询,实现原理都差不多,对于我实现的而言,都是调用外部api的形式,为大模型提供mcp服务。
空气质量查询service查询中采用的是一个免费的查询接口 OpenMeteo免费天气API ,通过发送请求,获取到指定经纬度地区的空气质量信息,返回给大模型做格式化输出。
对于 OpenMeteo免费天气API 的一些请求参数配置什么的,可以直接在其官网进行选择,小伙伴们自行研究
那么话不多说,直接看代码:
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/4/10 14:27
* @注释 天气服务
* <p>
* * 利用OpenMeteo的免费天气API提供空气质量服务
* * 该API无需API密钥,可以直接使用
*/
@Service
public class AirQualityService {
// OpenMeteo免费天气API基础URL
private static final String BASE_URL = "https://air-quality-api.open-meteo.com/v1";
private final RestClient restClient;
public AirQualityService() {
this.restClient = RestClient.builder().baseUrl(BASE_URL).defaultHeader("Accept", "application/json").defaultHeader("User-Agent", "OpenMeteoClient/1.0").build();
}
/**
* 获取指定位置的空气质量信息
*
* @param latitude 纬度
* @param longitude 经度
* @return 空气质量信息
*/
@Tool(description = "获取指定位置的空气质量信息")
public String getAirQuality(@ToolParam(description = "纬度") double latitude, @ToolParam(description = "经度") double longitude) {
try {
// 从空气数据中获取基本信息
var airData = restClient.get().uri("/air-quality?latitude=52.52&longitude=13.41&hourly=pm10,pm2_5¤t=european_aqi,us_aqi,pm10,pm2_5,carbon_monoxide,sulphur_dioxide,ozone,nitrogen_dioxide&timezone=Asia/Shanghai&forecast_hours=24&temporal_resolution=hourly_3", latitude, longitude).retrieve().body(AirData.class);
int europeanAqi = airData.current().europeanAqi();
int usAqi = airData.current().usAqi();
// 根据AQI评估空气质量等级
String europeanAqiLevel = getAqiLevel(europeanAqi);
String usAqiLevel = getUsAqiLevel(usAqi);
StringBuilder sb = new StringBuilder();
String format = String.format("""
空气质量信息:
位置: 纬度 %.4f, 经度 %.4f
欧洲空气质量指数: %d (%s)
美国空气质量指数: %d (%s)
PM10: %.1f μg/m³
PM2.5: %.1f μg/m³
一氧化碳(CO): %.1f μg/m³
二氧化氮(NO2): %.1f μg/m³
二氧化硫(SO2): %.1f μg/m³
臭氧(O3): %.1f μg/m³
数据更新时间: %s
""", latitude, longitude, europeanAqi, europeanAqiLevel, usAqi, usAqiLevel, airData.current().pm10, airData.current().pm2_5, airData.current().co, airData.current().no2, airData.current().so2, airData.current().o3, airData.current().time());
sb.append(format);
//解析forcast
sb.append("未来24H空气中的相关成分变化情况:\n");
if (airData.hourly() != null) {
AirData.HourlyForecast hourly = airData.hourly();
for (int i = 0; i < hourly.time.size(); i++) {
String date = hourly.time().get(i);
Double pm10 = hourly.pm10().get(i);
Double pm25 = hourly.pm2_5().get(i);
// 格式化日期
LocalDateTime localDate = LocalDateTime.parse(date);
String formattedDate = localDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
sb.append(String.format("""
%s:
PM10: %.1f μg/m³
PM2.5: %.1f μg/m³
""", formattedDate, pm10, pm25));
}
}
return sb.toString();
} catch (Exception e) {
// e.printStackTrace();
// 如果获取基本天气数据失败,返回完全模拟的数据
return String.format("""
空气质量信息获取失败:
位置: 纬度 %.4f, 经度 %.4f
请稍后重试~。
""", latitude, longitude);
}
}
/**
* 获取欧洲空气质量指数等级
*/
private String getAqiLevel(Integer aqi) {
if (aqi == null) return "未知";
if (aqi <= 20) return "优";
else if (aqi <= 40) return "良";
else if (aqi <= 60) return "中等";
else if (aqi <= 80) return "较差";
else if (aqi <= 100) return "差";
else return "极差";
}
/**
* 获取美国空气质量指数等级
*/
private String getUsAqiLevel(Integer aqi) {
if (aqi == null) return "未知";
if (aqi <= 50) return "优";
else if (aqi <= 100) return "中等";
else if (aqi <= 150) return "对敏感人群不健康";
else if (aqi <= 200) return "不健康";
else if (aqi <= 300) return "非常不健康";
else return "危险";
}
// OpenMeteo天气数据模型
@JsonIgnoreProperties(ignoreUnknown = true)
public record AirData(@JsonProperty("latitude") Double latitude, @JsonProperty("longitude") Double longitude,
@JsonProperty("timezone") String timezone, @JsonProperty("current") CurrentAir current,
@JsonProperty("current_units") CurrentUnits currentUnits,
@JsonProperty("hourly") HourlyForecast hourly) {
@JsonIgnoreProperties(ignoreUnknown = true)
public record CurrentAir(@JsonProperty("time") String time, @JsonProperty("interval") int interval,
@JsonProperty("european_aqi") int europeanAqi, @JsonProperty("us_aqi") int usAqi,
@JsonProperty("pm10") double pm10, @JsonProperty("pm2_5") double pm2_5,
@JsonProperty("carbon_monoxide") double co,
@JsonProperty("sulphur_dioxide") double so2, @JsonProperty("ozone") double o3,
@JsonProperty("nitrogen_dioxide") double no2) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record CurrentUnits(@JsonProperty("time") String timeUnit, @JsonProperty("interval") String intervalUnit,
@JsonProperty("european_aqi") String europeanAqiUnit,
@JsonProperty("us_aqi") String usAqiUnit, @JsonProperty("pm10") String pm10Unit,
@JsonProperty("pm2_5") String pm2_5Unit,
@JsonProperty("carbon_monoxide") String coUnit,
@JsonProperty("sulphur_dioxide") String so2Unit,
@JsonProperty("ozone") String o3Unit,
@JsonProperty("nitrogen_dioxide") String no2Unit) {
}
@JsonIgnoreProperties(ignoreUnknown = true)
public record HourlyForecast(@JsonProperty("time") List<String> time, @JsonProperty("pm10") List<Double> pm10,
@JsonProperty("pm2_5") List<Double> pm2_5) {
}
}
public static void main(String[] args) {
AirQualityService client = new AirQualityService();
// 北京空气质量(模拟数据)
System.out.println(client.getAirQuality(39.9042, 116.4074));
}
}
先简单的进行main 函数模拟调用一下:

看起来没啥大的问题,那我们进行下一步。
4、注册service为ToolCallbackProvider
将已经写好并且通过main函数测试的service,注册为mcp service的ToolCallbackProvider,可以让client知道,service 提供的 服务功能列表,我这边是有三个服务功能,就直接注册了三个。
/***
* @author jerryLau
* @version 1.0
* @Date 2025/4/10 16:24
* @注释 工具配置类
* 用于配置工具回调提供者,将工具方法注册为可调用的工具
*/
@Configuration
public class ToolConfiguration {
@Bean
public ToolCallbackProvider weatherTools(
AirQualityService airQualityService,
WeatherService weatherService,
CyberFortuneService cyberFortuneService) {
return MethodToolCallbackProvider
.builder()
.toolObjects(airQualityService, weatherService, cyberFortuneService)
.build();
}
}
5、打包server
将上述提供server的spring-boot 直接打包,通过 mvn package 进行打包,得到一个xxxxxxxxxxxx-0.0.1-SNAPSHOT.jar。到这一步stdio模式的server模块完成开发。
2.3.1.2 stdio-client
对于client部分,就仁者见仁智者见智了,在mcp官网有写Test方法进行连接测试的,有用第三方工具(cherry studio等)发布server进行测试的等。我们还是新建spring-boot项目进行client端的测试
⚠️同样跳过项目初始化阶段
1、项目依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-mcp-client-spring-boot-starter</artifactId>
<version>${mcp.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
</dependencies>
2、配置文件
application.yml
server:
port: 8089
spring:
application:
name: mcp-client-stdio
ai:
dashscope:
api-key: sk-xxxxxxx #阿里云百炼平台的api-key
mcp:
client:
stdio:
servers-configuration: classpath:/mcp-servers-config.json #指定在本路径下的mcp-servers-config.json文件,作为server的配置文件
mcp-servers-config.json
{
"mcpServers": {
"time-weather": { #time-weather这个名字随便
"command": "java", #启动server的命令:因为我们的server是jar包,所以java命令启动即 java -jar
"args": [
"-Dspring.ai.mcp.server.stdio=true", # 以什么方式启动,这里我们配置stdio方式
"-Dspring.main.web-application-type=none", #这两句其实就是server中配置的日志相关的,即关闭日志信息
"-Dlogging.pattern.console=",
"-jar",
"D:\\workspace\\code-demo\\langchain4j-demo\\spring-ai-alibaba-mcp-stdio-server\\target\\spring-ai-alibaba-mcp-stdio-server-0.0.1-SNAPSHOT.jar" #这个就是server中打的jar包的位置
],
"env": {}
}
}
}
3、使用server
为了简便,对于client直接在ClientApplication的启动文件中做如下代码填写
@SpringBootApplication
public class SpringAiAlibabaMcpStdioClientApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiAlibabaMcpStdioClientApplication.class, args);
}
@Bean
public CommandLineRunner predefinedQuestions(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools,
ConfigurableApplicationContext context) {
return args -> {
var chatClient = chatClientBuilder
.defaultTools(tools)
.build();
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.print("\n>>> QUESTION: ");
String userInput = scanner.nextLine();
if (userInput.equalsIgnoreCase("exit")) {
break;
}
String content = chatClient.prompt(userInput).call().content();
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
String str = new String(bytes, StandardCharsets.UTF_8);
System.out.println("\n>>> ASSISTANT: " + str);
}
scanner.close();
context.close();
};
}
}
大致可以理解为通过Spring-ai 创建了一个chatClient,通过获取server中注册得到的ToolCallbackProvider提供的一系列的tools配置到创建到的chatClient中。通过用户的命令行输入,匹配到相关的tools 进行调用server中的方法,返回结果给client中的模型,再由模型加工输出。
4、启动client
启动上述的clients-pring-boot程序,可以看到要求命令行输入,简单输入看到输出即可

再次询问”济南的空气质量如何“,会发现调用到的是server中的空气质量获取的方法,完成空气质量的输出

2.3.1.3 接入cherry studio
上面已经实现了本地client对于mcp server的验证,那么咱们再来简单的将这个mcp server集成到cherry studio中,进入前文中配置好的cherry studio,进入chat页面,选择阿里的千问-max模型,询问”济南今天天气怎么样“

可见 千问-max 不会直接回答我这个问题,那么我们来接入一下上文中实现的mcp服务器试试

⚠️⚠️⚠️
第九步点击保存的时候,正确的现象应该是会转一会儿圈,然后显示保存成功
可能会遇到以下问题:
- 报错3000x错误,请按照如下检查
- 添加时,java程序需要杀一下进程。进入win任务管理器,找到java相关的进程,先直接结束掉
- -jar后回车,再添加jar包路径
添加成功后,这个mcp服务器会默认设置为启用,在下拉后可以看到改mcp服务中的一些功能方法

继续打开chat页面,进行勾选mcp服务器

那我继续问他”济南今天的天气如何“时,可以看到大模型调用了我们的mcp服务器请求到了外部的接口获取到了天气预报信息。

那么至此呢 studio模式的mcp server 简单的搭建及演示结束。
那个人感觉比较难受的是:
stdio使用的是java -jar的方式运行jar包;
cherry得解析到jar包;
通过java -jar启动;
通过输入访问server中的方法;
进行外部调用;
时间上明显是比较费时的,在加载好久之后才能返回结果信息
2.3.2 sse
相比于stdio的server需要打jar包,sse模式的server一般是像springboot程序一样进行本地运行,只需要提供调用链接到client即可
2.3.2.1 sse-server
1、引入依赖
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai-alibaba.version}</version>
<exclusions>
<exclusion>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-autoconfigure</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.57</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
<version>${spring-ai-version}</version>
</dependency>
</dependencies>
如果存在拉取镜像拉取失败的情况,请参考 stdio-server 的
2、配置文件
application.yml
server:
port: 8085 #开放port
spring:
application:
name: mcp-server-sse
main:
banner-mode: off
ai:
dashscope:
api-key: sk-xxxxxxxxx #阿里云百炼api-key
mcp:
server:
name: balance
version: 0.0.1
# 调试日志
logging:
level:
io:
modelcontextprotocol:
client: DEBUG
spec: DEBUG
3、编写service
此处我们换一个service,编写一个service查询我的deepseek 账户余额信息,具体代码如下
/**
* @version 1.0
* @Author jerryLau
* @Date 2025/4/11 17:19
* @注释
*/
@Service
public class BalanceService {
private static final String BASE_URL = "https://api.deepseek.com";
private final RestClient restClient;
public BalanceService() {
this.restClient = RestClient.builder()
.baseUrl(BASE_URL)
.defaultHeader("Accept", "application/json")
.defaultHeader("Authorization", "Bearer sk-XXXXXXX") //你自己的deepseek的api-key
.build();
}
@Tool(description = "获取我的深度求索账户余额")
public String getBalance() {
try {
JSONObject body = restClient.get()
.uri("/user/balance")
.retrieve().body(JSONObject.class);
return body.toString();
} catch (Exception e) {
e.printStackTrace();
return "获取余额失败";
}
}
public static void main(String[] args) {
BalanceService balanceService = new BalanceService();
String balance = balanceService.getBalance();
System.out.println(balance);
}
}
通过main方法查询到我的个人账户信息以及余额为9.97元

至此 我们server-sse 代码编写完成,只需要启动这个spring-boot工程即可
2.3.2.2 集成cherry studio
sse-client我这边就不具体实现了,唯一变化的地方在于删除了jar包运行的配置文件:mcp-servers-config.json
application.yml配置如下:
server:
port: 8087
spring:
application:
name: mcp-client-sse
main:
web-application-type: none
ai:
dashscope:
api-key: sk-xxxxxxxxxxxxxx
mcp:
client:
sse:
connections:
server:
url: http://localhost:8086 # sse sercer的地址
接下来我们直接将sse-server集成到cherry studio
首先保证sse-server启动,目前我这边启动到的端口是8086端口

然后根据一下步骤进行cherry studio 的配置 [写到一半…cherry studio更新了,可能界面和前面的不一致,领会精神就行]

配置好后可以在功能明细中查询到

我们优先尝试询问大模型 ”我的账户余额是多少“,可见回答的不是我们想要的内容。

尝试使用mcp服务器

继续询问上述问题:

那么 至此 欢呼吧 雀跃吧 🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉
代码仓库 GitHub🌐🌐🌐🌐
6990

被折叠的 条评论
为什么被折叠?



