Spring AI 进阶之路05:集成 MCP 协议实现工具调用

引子

在上一篇文章中,我们通过集成 SearXNG,成功让大模型"睁眼看世界",具备了获取互联网实时信息的能力。然而,无论是 RAG(检索增强生成)还是联网搜索,本质上都是让 AI "读" 更多的书,获取更多的信息。但一个真正的智能助手,不仅要能"读",还要能 "写""做"

试想这样一个场景:你希望 AI 帮你整理今天的股市数据,并生成一份 Excel 报表保存到桌面;或者你希望 AI 帮你给客户发送一封会议邀请邮件。在目前的架构下,大模型只能告诉你"邮件内容写好了,请你复制粘贴去发送",它就像一个被困在罐子里的"超级大脑",虽然博学,却无法触碰现实世界。

为了打破这个次元壁,我们需要引入 MCP(Model Context Protocol,模型上下文协议) 。关于它的概念不多赘述概念,网上相关的文章已经很多了,有需要了解请看MCP中文文档:docs.mcpcn.org/introductio...

本文将分为两个部分实战 MCP:

  1. 作为客户端(Client):调用现成的 MCP 服务。
  2. 作为服务端(Server):开发我们自己的 MCP 服务。

调用 MCP 服务:操作本地文件

Spring AI 提供了 spring-ai-mcp-client,允许我们的应用连接到任何遵循 MCP 标准的服务器。这里我们以官方提供的 文件系统 MCP 服务器 为例,让 AI 具备在本地创建和读取文件的能力。

前提条件:由于文件系统 MCP 服务是基于 Node.js 的,请确保你的本地环境已安装 Node.js (v18+)。

1.添加依赖

pom.xml 中引入 MCP Client 相关的依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-mcp</artifactId>
</dependency>

2. 配置 MCP Server 连接

MCP 支持两种连接模式:stdio(标准输入输出,适用于本地进程)和 sse(Server-Sent Events,适用于网络服务)。对于本地文件系统服务,我们使用 stdio 模式。

resources 目录下新建 mcp-server.json 文件,定义如何启动文件系统服务:

json 复制代码
{
  "mcpServers": {
    "filesystem": {
      "command": "D:\\devolop\\node\\npx.cmd",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "D:\\devolop"
      ]
    }
  }
}

配置解析

  • command: 指向你的 npx 可执行文件路径(Windows 下通常是 npx.cmd)。
  • args:
    • -y: 自动确认安装。
    • @modelcontextprotocol/server-filesystem: 官方的文件系统 MCP 服务包。
    • D:\\devolop: 这是允许 AI 访问的根目录。为了安全起见,AI 只能操作这个目录及其子目录下的文件。

接着,在 application.yml 中启用 MCP Client 并加载上述配置:

yaml 复制代码
spring:
  ai:
    mcp:
      client:
        enabled: true
        name: spring-ai-mcp-client
        type: ASYNC # 推荐使用异步非阻塞模式
        stdio:
          servers-configuration: classpath:mcp-server.json

3.代码改造

我们需要在 ChatService 初始化时,将 MCP Client 发现的工具注册到 ChatClient 中。

java 复制代码
public ChatServiceImpl(ChatClient.Builder chatClientBuilder, ToolCallbackProvider tools) {
   // tools 会自动注入所有配置好的 MCP 工具
   this.chatClient = chatClientBuilder
           .defaultToolCallbacks(tools)
           .build();
}

4.效果测试

启动项目,观察控制台日志,可以看到MCP工具已经连接成功。

现在,我们在对话框中输入这些内容:

打开我们配置的 D:\devolop 目录,可以看到文件已经被成功创建。

开发 MCP 服务:邮件与时间工具

除了调用现有的工具,更常见的场景是将我们自己的业务逻辑(如查询内部系统、发送通知)封装成 MCP 工具供大模型调用。

为了演示,我们创建一个新的模块 mcp-server,实现"获取当前时间"和"发送邮件"两个功能。

mcp-server 模块的 pom.xml 中添加必要的依赖。除了 WebFlux(MCP Server 常用底层),我们还需要 spring-boot-starter-mail 来发送邮件,以及 flexmark 用于将 AI 生成的 Markdown 内容转换为邮件友好的 HTML。

xml 复制代码
<?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.cc</groupId>
        <artifactId>SpringAI-MCP-RAG-Dev</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>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- MCP Server 依赖 -->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-mcp-server-webflux</artifactId>
        </dependency>

        <!-- 邮件发送依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

        <!-- Markdown 转 HTML 工具 -->
        <dependency>
            <groupId>com.vladsch.flexmark</groupId>
            <artifactId>flexmark-all</artifactId>
            <version>0.64.8</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.17.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

1.添加配置

application.yml 中配置 OpenAI Key、Redis(用于向量存储或缓存)以及 SMTP 邮件服务信息。

yaml 复制代码
spring:
  application:
    name: spring-ai-mcp-server
  data:
    redis:
      host: 127.0.0.1
      port: 9379
      password: 123456
  ai:
    mcp:
      server:
        name: spring-ai-mcp-server-sse
        version: 1.0.0
        sse-endpoint: /sse
        type: async
  mail:
    host: smtp.163.com
    port: 465
    username: 123@163.com
    password: 123456  # 注意:这里通常是邮箱授权码,不是登录密码
    protocol: smtp
    default-encoding: UTF-8
    properties:
      mail:
        smtp:
          socketFactory:
            port: 465
            class: javax.net.ssl.SSLSocketFactory
          ssl:
            enable: true


logging:
  level:
    root: info
server:
  port: 6080

启动项目,访问 http://localhost:6080/sse,如果能正常启动,说明配置无误。

2. 开发时间查询工具

大模型本身对"现在是几点"没有概念,我们需要提供一个工具。

java 复制代码
package com.cc.mcp.tool;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
@Slf4j
public class DateTool {

    // @Tool 注解将方法暴露为 MCP 工具
    // description 非常重要,大模型根据它来判断何时调用此工具
    @Tool(description = "获取当前时间")
    public String getCurrentTime() {
        log.info("=================调用MCP工具:获取当前时间=================");
        return String.format("当前的时间是 %s", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }

}

3. 开发邮件发送工具

这是一个稍微复杂一点的工具,需要定义参数结构。

java 复制代码
package com.cc.mcp.tool;

import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.data.MutableDataSet;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class EmailTool {

    private final JavaMailSender mailSender;
    private final String from;

    @Autowired
    private EmailTool(JavaMailSender mailSender, @Value("${spring.mail.username}") String from) {
        this.mailSender = mailSender;
        this.from = from;
    }

    // 定义请求参数类,大模型会自动填充这些字段
    @Data
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class EmailRequest {
        @ToolParam(description = "收件人邮箱地址")
        private String email;
        
        @ToolParam(description = "发送邮件的标题/主题")
        private String subject;
        
        @ToolParam(description = "发送邮件的消息/正文内容")
        private String message;
        
        @ToolParam(description = "发送邮件的内容类型,1为HTML格式,2为普通文本格式")
        private Integer contentType;
    }

    @Tool(description = "给指定邮箱发送邮件信息。")
    public String sendEmail(EmailRequest emailRequest) {
        log.info("=================调用MCP工具:sendEmail=================");
        log.info("请求详情: {}", emailRequest);

        Integer contentType = emailRequest.getContentType();

        try {
            MimeMessage mimeMessage = mailSender.createMimeMessage();
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage);
            
            mimeMessageHelper.setFrom(from);
            mimeMessageHelper.setTo(emailRequest.getEmail());
            mimeMessageHelper.setSubject(emailRequest.getSubject());
            
            // 智能处理:如果是 Markdown 格式,自动转 HTML
            if (contentType != null && contentType == 1) {
                mimeMessageHelper.setText(convertMarkdownToHtml(emailRequest.getMessage()), true);
            } else if (contentType != null && contentType == 2) {
                mimeMessageHelper.setText(emailRequest.getMessage(), true);
            } else {
                // 默认处理
                mimeMessageHelper.setText(emailRequest.getMessage());
            }
            
            mailSender.send(mimeMessage);
            return "邮件发送成功";
            
        } catch (MessagingException e) {
            log.error("发送邮件失败", e);
            return "发送邮件失败: " + e.getMessage();
        }
    }

    /**
     * 将Markdown格式的字符串转换为HTML格式
     */
    public static String convertMarkdownToHtml(String markdownStr) {
        MutableDataSet dataSet = new MutableDataSet();
        Parser parser = Parser.builder(dataSet).build();
        HtmlRenderer htmlRenderer = HtmlRenderer.builder(dataSet).build();
        return htmlRenderer.render(parser.parse(markdownStr));
    }
}

4. 注册工具

最后,在启动类或配置类中,将我们编写的 Tool Bean 注册到 ToolCallbackProvider 中。

typescript 复制代码
package com.cc.mcp;

import com.cc.mcp.tool.DateTool;
import com.cc.mcp.tool.EmailTool;
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;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    /**
     * 注册自定义 MCP 工具
     * 这样 ChatClient 就能感知到这些工具的存在
     */
    @Bean
    public ToolCallbackProvider registerMCPTools(DateTool dateTool, EmailTool emailTool) {
        return MethodToolCallbackProvider.builder()
                .toolObjects(dateTool, emailTool)
                .build();
    }
}

5. 综合测试

启动项目,观察日志,确认工具已加载。

测试时间查询:

测试邮件发送:

增加记忆功能:让对话更连贯

在实际使用中,我们可能会分多轮对话来完成任务。但默认情况下,ChatClient 是无状态的,它记不住上一句说了什么:

为了解决这个问题,我们需要引入 Chat Memory 。在 ChatServiceImpl 中注入 ChatMemory,并将其配置到 ChatClient 中:

java 复制代码
private final ChatClient chatClient;

    // 注入 ChatMemory
    public ChatServiceImpl(ChatClient.Builder chatClientBuilder, 
                           ToolCallbackProvider tools, 
                           ChatMemory chatMemory) {
        this.chatClient = chatClientBuilder
                // 添加记忆 Advisor
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .defaultToolCallbacks(tools)
                .build();
    }

配置完成后,模型就具备了上下文记忆能力,能够流畅地处理多轮意图确认。

小结

通过本篇文章,我们实现了 Spring AI 应用功能的重大飞跃:从单纯的"信息获取者"进化为了"任务执行者"。

  1. 利用 MCP Client,我们轻松集成了现有的文件系统服务。
  2. 利用 Spring AI Tool,我们开发了自定义的邮件和时间服务。
  3. 利用 Chat Memory,我们赋予了 AI 记忆,使其交互更加自然。

现在,我们的 AI 已经可以操作文件、发送邮件了。但在企业级应用中,最核心的数据往往存储在数据库中。如何让大模型安全、准确地查询和操作数据库?下一篇,我们将探讨基于 MCP 的大模型与数据库交互开发

相关推荐
ss2732 小时前
线程池:任务队列、工作线程与生命周期管理
java·后端
不像程序员的程序媛2 小时前
Spring的cacheEvict
java·后端·spring
踏浪无痕3 小时前
JobFlow 实战:无锁调度是怎么做到的
后端·面试·架构
shoubepatien3 小时前
JAVA -- 11
java·后端·intellij-idea
喵个咪3 小时前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:kratos-bootstrap 入门教程(类比 Spring Boot)
后端·微服务·go
uzong3 小时前
从大厂毕业后,到小公司当管理,十年互联网老兵的思维习惯阶段复盘
后端
追逐时光者3 小时前
一个 WPF 开源、免费的 SVG 图像查看控件
后端·.net
谷哥的小弟4 小时前
Spring Framework源码解析——PropertiesLoaderUtils
java·后端·spring·框架·源码
JIngJaneIL4 小时前
基于java+ vue助农电商系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端