基于Spring AI构建MCP服务
编辑基于Spring AI构建MCP服务
前言
最近在预研MCP的使用,计划与我们的业务功能进行整合。本篇简单梳理下基于Spring AI框架下如何构建MCP服务。
使用到的组件以及版本
基本流程图
具体实现步骤
项目整体的结构
mcp-server
项目依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yzh</groupId>
<artifactId>mcp</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>mcp-server</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml配置文件
spring:
ai:
mcp:
server:
name: mcp-server
server:
port: 8777
在toolService中提供了三个工具方法
package com.yzh;
import cn.hutool.extra.ssh.JschUtil;
import com.jcraft.jsch.Session;
import com.yzh.model.MysqlEntity;
import com.yzh.model.SshEntity;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 提供Function Calling
*
* @author yuanzhihao
* @since 2025/4/25
*/
@Service
public class ToolService {
// 模拟业务查询
private static final Map<String, SshEntity> SSH_STORE = Map.of();
private static final Map<String, MysqlEntity> MYSQL_STORE = Map.of();
@Tool(name = "执行shell命令并且获取输出")
public String execCommand(String host, String commands) {
SshEntity sshEntity = SSH_STORE.get(host);
if (Objects.isNull(sshEntity)) {
throw new IllegalArgumentException("主机不存在!!!");
}
Session session = JschUtil.getSession(host, sshEntity.getPort(), sshEntity.getUsername(), sshEntity.getPassword());
return JschUtil.exec(session, commands, StandardCharsets.UTF_8);
}
@Tool(name = "执行mysql命令并且获取输出")
public List<Map<String, Object>> execMysql(String host, String database, String sqlStatement) {
MysqlEntity mysqlEntity = MYSQL_STORE.get(host);
if (Objects.isNull(mysqlEntity)) {
throw new IllegalArgumentException("Mysql主机不存在!!!");
}
JdbcTemplate jdbcTemplate = buildJdbcTemplate(mysqlEntity, database);
try (HikariDataSource ignored = (HikariDataSource) jdbcTemplate.getDataSource()) {
return jdbcTemplate.queryForList(sqlStatement);
}
}
@Tool(name = "执行命令,不需要返回,比如kill某一个进程")
public void execMysqlCommand(String host, String database, String command) {
MysqlEntity mysqlEntity = MYSQL_STORE.get(host);
if (Objects.isNull(mysqlEntity)) {
throw new IllegalArgumentException("Mysql主机不存在!!!");
}
JdbcTemplate jdbcTemplate = buildJdbcTemplate(mysqlEntity, database);
try (HikariDataSource ignored = (HikariDataSource) jdbcTemplate.getDataSource()) {
jdbcTemplate.update(command);
}
}
private JdbcTemplate buildJdbcTemplate(MysqlEntity mysqlEntity, String database) {
HikariConfig config = new HikariConfig();
String jdbcUrl = "jdbc:mysql://" + mysqlEntity.getHost() + ":" + mysqlEntity.getPort() + "/" +
database + "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai&characterEncoding=utf8";
config.setJdbcUrl(jdbcUrl);
config.setUsername(mysqlEntity.getUsername());
config.setPassword(mysqlEntity.getPassword());
config.setMaximumPoolSize(2); // 设置最大连接数
config.setMinimumIdle(1); // 设置最小空闲连接数
config.setIdleTimeout(30000); // 设置空闲超时
config.setConnectionTimeout(30000); // 设置连接超时
config.setMaxLifetime(1800000);// 设置连接最大生命周期
HikariDataSource hikariDataSource = new HikariDataSource(config);
return new JdbcTemplate(hikariDataSource);
}
}
将toolService加载到spring容器
@Bean
public ToolCallbackProvider toolCallbackProvider(ToolService toolService) {
return MethodToolCallbackProvider.builder().toolObjects(toolService).build();
}
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.yzh</groupId>
<artifactId>mcp</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>mcp-client</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
</dependencies>
</project>
application.yml配置文件
spring:
ai:
openai:
api-key: ${your_api_key}
base-url: https://api.deepseek.com
chat:
options:
model: deepseek-chat
mcp:
client:
sse:
connections:
server1:
url: http://localhost:8777 # mcp-server的地址 用于发现tools
toolcallback:
enabled: true # 是否开启工具回调 这个需要设置为true
name: mcp-client
request-timeout: 30s
server:
port: 8666
使用http接口作为调用发起的方式,具体代码
package com.yzh;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
/**
* mcp client 启动类
*
* @author yuanzhihao
* @since 2025/4/25
*/
@RestController
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
@Resource
ChatClient.Builder chatClientBuilder;
/**
* 工具回调提供
*/
@Resource
SyncMcpToolCallbackProvider toolCallbackProvider;
private ChatClient chatClient;
@GetMapping
public String request(@RequestParam("question") String question) {
if (Objects.isNull(chatClient)) {
this.chatClient = chatClientBuilder
.defaultTools(toolCallbackProvider)
.build();
}
return chatClient.prompt(question).call().content();
}
}
调用
有哪些工具
分析linux主机的性能并且给出建议
模拟一个mysql的慢查,分析并且kill掉具体的进程
遇到的一些问题
必须要先启动mcp-server,然后再启动mcp-client
如果mcp-server变更了重新启动的话,mcp-client调用日志会报404,需要重新启动mcp-client才行,参考:https://github.com/spring-projects/spring-ai/issues/2785
SYNC模式下ChatClient不能重复创建,会抛异常,参考:https://github.com/spring-projects/spring-ai/issues/2422
结语
参考:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html
代码地址:https://github.com/yzh19961031/blogDemo/tree/master/mcp