Yuandupier

Yuandupier

基于Spring AI构建MCP服务

42
0
0
2025-04-27

基于Spring AI构建MCP服务

前言

最近在预研MCP的使用,计划与我们的业务功能进行整合。本篇简单梳理下基于Spring AI框架下如何构建MCP服务。

使用到的组件以及版本

组件名称

版本

JDK

17

Spring Boot

3.3.4

Spring AI

1.0.0-M7

deepseek

deepseek-chat

基本流程图

具体实现步骤

项目整体的结构

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

结语

参考:https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html

代码地址:https://github.com/yzh19961031/blogDemo/tree/master/mcp