Spring AI Alibaba Graph框架实现MCP数据库查询实战

目录

前言

一、环境准备

[1.1 技术栈](#1.1 技术栈)

[1.2 创建数据库表](#1.2 创建数据库表)

二、依赖与配置避坑

[三、Server 端开发:把数据库操作暴露为工具](#三、Server 端开发:把数据库操作暴露为工具)

[3.1 配置文件](#3.1 配置文件)

[3.2 实体与 Mapper](#3.2 实体与 Mapper)

[3.3 定义数据库工具类](#3.3 定义数据库工具类)

[3.4 注册工具](#3.4 注册工具)

[四、Client 端开发:解决时序死锁并调用工具](#四、Client 端开发:解决时序死锁并调用工具)

[4.1 手动连接管理器 LocalMcpManager](#4.1 手动连接管理器 LocalMcpManager)

[4.2 构建带工具的 ChatClient](#4.2 构建带工具的 ChatClient)

[4.3 对话接口](#4.3 对话接口)

五、测试效果

六、核心踩坑总结

七、总结


前言

这是 Spring AI Alibaba 进阶系列的第五篇。在上一篇中,我们完成了 MCP 协议的全链路集成,让一个应用通过 SSE 把工具能力暴露出去,另一个应用作为 Client 动态发现并调用这些工具。

但在企业场景里,MCP 最大的价值在于让大模型能够操作真实的业务数据 。本文我们将通过一个**单应用自连(8081 既做 Server 又做 Client)**的实战,把 MySQL 数据库的查询封装成 MCP 工具,让大模型真正执行 SQL 并返回结果。

单应用自连的优势 :只需启动一个进程即可验证完整协议,调试极其方便。

单应用自连的难点:必须手动处理启动时序死锁(Server 还没起好时,Client 不能去连)。

一、环境准备

1.1 技术栈

组件 版本
JDK 17
Spring Boot 3.3.x
Spring AI 1.0.3
MySQL 5.7 或 8.0
MyBatis-Plus 3.5.7

1.2 创建数据库表

bash 复制代码
CREATE DATABASE mcp_demo;
USE mcp_demo;

CREATE TABLE product (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL COMMENT '商品名称',
    price DECIMAL(10, 2) NOT NULL COMMENT '商品价格',
    stock INT NOT NULL DEFAULT 0 COMMENT '库存数量'
);

INSERT INTO product (name, price, stock) VALUES ('MacBook Pro', 12999.00, 50);
INSERT INTO product (name, price, stock) VALUES ('iPhone 15', 5999.00, 200)

二、依赖与配置避坑

在单体应用中,WebMVC 和 WebFlux 的依赖绝对不能混用。必须彻底抛弃 WebFlux 版本,否则会导致 context-path 失效、SSE 端点无法映射。

完整 POM 依赖:

XML 复制代码
<!-- 1. Web 基础依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 2. MCP Server:必须是 WebMVC 版本 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>

<!-- 3. MCP Client:基于标准 HTTP 版本 -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

<!-- 4. 数据库相关 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

三、Server 端开发:把数据库操作暴露为工具

3.1 配置文件

bash 复制代码
server.port=8081
server.servlet.context-path=/ygc

# ========== 数据库配置 ==========
spring.datasource.url=jdbc:mysql://localhost:3306/mcp_demo?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=your_password

# ========== MCP Server 配置 ==========
spring.ai.mcp.server.name=db-mcp-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.type=ASYNC
spring.ai.mcp.server.base-url=/db-mcp
spring.ai.mcp.server.sse-endpoint=/sse
spring.ai.mcp.server.sse-message-endpoint=/mcp/message

# ========== ★ 关键:关闭客户端自动配置,稍后手动连接 ==========
spring.ai.mcp.client.enabled=false

# ===================== MyBatis-Plus =====================
mybatis-plus.type-aliases-package=your_package
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.global-config.db-config.id-type=AUTO
mybatis-plus.global-config.db-config.logic-delete-field=isDelete
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0

3.2 实体与 Mapper

java 复制代码
@Data
@TableName("product")
public class Product {
    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stock;
    //get set..
}
java 复制代码
public interface ProductMapper extends BaseMapper<Product> {
    // 无需额外定义,BaseMapper 提供了 CRUD 方法
}

3.3 定义数据库工具类

java 复制代码
@Component
@Slf4j
public class DatabaseTools {

    @Resource
    private ProductMapper productMapper;

    @Tool(description = "查询所有商品列表,返回商品名称、价格和库存")
    public String listAllProducts() {
        log.info("调用MCP工具:listAllProducts()");
        List<Product> products = productMapper.selectList(null);
        return products.stream()
                .map(p -> String.format("商品:%s,价格:%s,库存:%d",
                        p.getName(), p.getPrice(), p.getStock()))
                .collect(Collectors.joining("\n"));
    }

    @Tool(description = "根据商品名称模糊查询商品信息,参数为商品名称关键字")
    public String searchProductByName(
            @ToolParam(description = "商品名称关键字,如 MacBook") String keyword) {
        log.info("调用MCP工具:searchProductByName(keyword={})", keyword);
        List<Product> products = productMapper.selectList(
                new LambdaQueryWrapper<Product>().like(Product::getName, keyword));
        if (products.isEmpty()) {
            return "未找到匹配的商品";
        }
        return products.stream()
                .map(p -> String.format("商品:%s,价格:%s,库存:%d",
                        p.getName(), p.getPrice(), p.getStock()))
                .collect(Collectors.joining("\n"));
    }
}

3.4 注册工具

java 复制代码
@Configuration
public class McpServerConfig {

    @Bean
    public ToolCallbackProvider toolCallbackProvider(DatabaseTools databaseTools) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(databaseTools)
                .build();
    }
}

启动 Server 应用,日志中应打印 Registered tools: 2,说明数据库查询工具已经通过 /db-mcp/sse 成功暴露。

四、Client 端开发:解决时序死锁并调用工具

在同一个应用里自连,最大的坑在于:如果开启 spring.ai.mcp.client.enabled=true,客户端会在 Tomcat 还没监听端口时去连接自己,直接报 Connection refused。

所以我们的策略是:关闭自动配置,等应用完全启动后,手动接管连接。

4.1 手动连接管理器 LocalMcpManager

利用 ApplicationReadyEvent 在 Tomcat 就绪后发起连接:

java 复制代码
@Component
@Slf4j
public class LocalMcpManager {

    private ToolCallbackProvider toolCallbackProvider;

    public ToolCallbackProvider getToolCallbackProvider() {
        return this.toolCallbackProvider;
    }

    @EventListener(ApplicationReadyEvent.class)
    public void init() {
        try {
            Thread.sleep(1000); // 等 Tomcat 彻底就绪
            log.info("========== 应用已启动,开始连接本地 MCP Server ==========");

            // 路径拼接:baseUrl 只写端口,sseEndpoint 写完整绝对路径
            McpClientTransport transport = HttpClientSseClientTransport
                    .builder("http://localhost:8081")
                    .sseEndpoint("/ygc/sse")
                    .build();

            McpSyncClient client = McpClient.sync(transport)
                    .requestTimeout(Duration.ofSeconds(60))
                    .clientInfo(new McpSchema.Implementation("local-client", "1.0.0"))
                    .build();

            client.initialize();

            this.toolCallbackProvider = new SyncMcpToolCallbackProvider(List.of(client));
            log.info("========== MCP Client 初始化成功,已获取工具 ==========");
        } catch (Exception e) {
            log.error("MCP Client 连接失败: ", e);
        }
    }
}

4.2 构建带工具的 ChatClient

由于此时 MCP 已经握手成功,我们可以在 McpClientConfig 里直接注入 ToolCallbackProvider 构建 ChatClient:

java 复制代码
@Configuration
public class McpClientConfig {

    /**
     * 将 MCP Client 发现的所有远端工具(来自本地 Server)注册到 ChatClient。
     * 这里的 ToolCallbackProvider 由 Spring AI 自动注入,
     * 它包含了 DateTool 等所有在 Server 端注册的工具。
     */
    @Bean("mcpChatClient")
    public ChatClient mcpChatClient(ChatModel chatModel,ToolCallbackProvider toolCallbackProvider) {
        return ChatClient.builder(chatModel)
                .defaultToolCallbacks(toolCallbackProvider.getToolCallbacks())
                .build();
    }
}

4.3 对话接口

java 复制代码
@RestController
@RequestMapping("/api/ai")
public class AiController {

    @Resource
    @Qualifier("mcpChatClient")
    private ChatClient chatClient;

    @GetMapping("/chat")
    public Map<String, String> chat(@RequestParam("message") String message) {
        String answer = chatClient.prompt()
                .user(message)
                .call()
                .content();
        return Map.of("answer", answer);
    }
}

五、测试效果

启动应用,你会看到两个关键日志:

  1. Registered tools: 2(Server 端工具准备完毕)

  2. MCP Client 初始化成功,已获取工具(Client 端握手成功)

测试 1:全量查询

java 复制代码
curl -G "http://localhost:8081/db-mcp/api/ai/chat" --data-urlencode "message=数据库里有哪些商品?"

返回:

bash 复制代码
{
    "answer": "数据库中有以下商品:\n\n- MacBook Pro:价格 12999.00 元,库存 50 台  \n- iPhone 15:价格 5999.00 元,库存 200 台"
}

测试 2:条件查询

java 复制代码
curl -G "http://localhost:8081/db-mcp/api/ai/chat" --data-urlencode "message=帮我查一下名字里带 iPhone 的商品还有多少库存?"

返回:

bash 复制代码
{
    "answer": "名字里带 iPhone 的商品是 iPhone 15,当前库存为 200 台。"
}

六、核心踩坑总结

现象 根因 解法
Connection refused 客户端在 Tomcat 监听前发起连接 设置 spring.ai.mcp.client.enabled=false,用 ApplicationReadyEvent 延迟连接
POST 消息 404 sse-message-endpoint 带前导斜杠导致前缀丢失 写成相对路径 mcp/message
路径解析丢失 /db-mcp WebFlux 客户端 URI.resolve 行为不可控 换标准 mcp-client,用"端口 + 完整绝对路径"
Client must be initialized before listing tools ChatClient 在 MCP 握手前就解析工具 确保 LocalMcpManager 在应用就绪后再赋值 ToolCallbackProvider

七、总结

本文在单体应用(8081)中跑通了 MCP Server + MCP Client 的数据库查询全链路。核心要点:

  1. Server 端 :用 @Tool 封装数据库查询,通过 MethodToolCallbackProvider 注册。

  2. Client 端 :因为是自己连自己,必须关闭自动配置,用 ApplicationReadyEvent 延迟握手。

  3. 大模型:通过自然语言提问,大模型自主决定查哪个表、传什么条件,Server 执行真实 SQL,最终返回自然语言答案。

如果你想把数据库换成其他类型(PostgreSQL、Oracle、MongoDB),只需要替换 Server 端的数据访问层,Client 端和 MCP 协议层完全不用改。

系列文章:

相关推荐
古法老登瞎折腾1 小时前
大数据智能运维实战(九):让 AI 查日志——找到异常,也保留上下文
java·大数据·运维·人工智能·spring ai·mcp
IT_陈寒1 小时前
刚解决一个 Java 并发问题,这坑踩得我怀疑人生
前端·人工智能·后端
ai大模型-探索者1 小时前
Function Call与MCP(区别、联系与实战)
java·服务器·网络
略略略咯咯1 小时前
啊啊啊啊啊
java·服务器·数据库
小兔子2 小时前
Unix domain socket 的四个边界:抽象地址没有权限,SO_PEERCRED 是快照
后端
小兔子2 小时前
no-cache 不是「不缓存」:RFC 9111 里几句常被读反的话
后端
Vecter2 小时前
于 Spring Boot 和 LangChain4j 的 Web 智能助手
后端
张一凡932 小时前
把“不要乱改”从提示词里拿出来,放进可校验的提案里
前端·后端·架构
一只爱撸猫的程序猿2 小时前
当任何人都能生成一万行代码:什么变得稀缺
人工智能·后端·程序员