SpringBoot 整合 MCP

news2025/5/13 14:05:01

SpringBoot 整合 MCP

MCP

MCP 协议主要分为:

  • Client 客户端(一般就是指 openai,deepseek 这些大模型)
  • Server 服务端(也就是我们的业务系统)我们要做的就是把我们存量系统配置成 MCP Server

环境

  • JDK17
  • SpringBoot 3

引入依赖

        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-core</artifactId>
            <version>1.0.0-M6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-spring-boot-autoconfigure</artifactId>
            <version>1.0.0-M6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
            <version>1.0.0-M6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-mcp-server-webmvc-spring-boot-starter</artifactId>
            <version>1.0.0-M6</version>
        </dependency>

配置 yaml

spring:
  ai:
    openai:
      base-url: https://api.deepseek.com
      api-key: sk-xxxxxxxx			# deepseek 的 api-key
      chat:
        enabled: true
        options:
          model: deepseek-chat		# 使用这个模型
          temperature: 0.7
          stream-usage: true		# 有的模型不支持

logging:
  level:
    org.springframework.ai: debug	# 开启 debug,打印思考链路

工具类

工具类的作用就是获取 springboot 里所有需要注册的 bean,这里是策略是 获取所有 “Controller”, “Service”, “Manager” 结尾的 bean,可以自行修改。

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Spring 框架工具类
 *
 * @author wen7.online
 */
@Slf4j
@Component
public class SpringTools{
    @Resource
    private ApplicationContext applicationContext;


    /**
     * 获取所有 "Controller", "Service", "Manager" 结尾的 bean,里面的 @Tool 注解的方法作为大模型上下文 MCP
     *
     * @return 所有 "Controller", "Service", "Manager" 结尾的 bean
     */
    public List<Object> findToolCallbackBeans() {
        String[] suffixes = {"Controller", "Service", "Manager"};
        String[] excludeNames = {"AiController"};		//这里是因为在 AiController 里循环引用了

        Set<String> excludeSet = Arrays.stream(excludeNames).collect(Collectors.toSet());

        return Arrays.stream(applicationContext.getBeanNamesForAnnotation(Component.class))
                .filter(beanName -> {
                    log.info("beanName: {}", beanName);
                    Class<?> type = applicationContext.getType(beanName);
                    if (type == null) return false;

                    String simpleName = type.getSimpleName();
                    if (excludeSet.contains(simpleName)) return false;

                    return Arrays.stream(suffixes)
                            .anyMatch(simpleName.replace("$$SpringCGLIB$$0","")::endsWith);        //有可能获取的是代理对象,$$SpringCGLIB$$0 结尾
                })
                .map(applicationContext::getBean)
                .collect(Collectors.toList());
    }

    public Object unwrapProxy(Object bean) {
        if (AopUtils.isAopProxy(bean)) { // 检查是否是代理对象
            try {
                Object target = ((Advised) bean).getTargetSource().getTarget();
                // 递归解包,确保多层代理情况下能获取到最终原始对象
                return unwrapProxy(target);
            } catch (Exception e) {
                return bean;
            }
        }
        return bean; // 非代理对象直接返回
    }

}

配置类

mport com.quick.common.utils.spring.SpringTools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.List;

/**
 * ChatClient 配置
 */
@Slf4j
@Configuration
public class ChatClientConfiguration {

    @Bean
    public ToolCallbackProvider toolCallbackProvider(SpringTools springTools) {
        List<Object> toolObjects = springTools.findToolCallbackBeans().stream()
                .map(springTools::unwrapProxy)  // 获取源对象,防止代理原因
                .toList();
		
        //核心,把所有的 bean 注入,会自动读取 @Tool 注解
        MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
                .toolObjects(toolObjects.toArray())
                .build();

        List<ToolCallback> tools = Arrays.stream(provider.getToolCallbacks()).toList();
        tools.stream().forEach(tool->{
            log.info("Register Tool: {}.{}", tool.getName(),tool.getDescription());
        });
        return provider;
    }


    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, ToolCallbackProvider toolCallbackProvider) {
        return builder
                .defaultSystem("""
                        本系统是一个 SaaS 平台,分为平台,租户,用户
                        每次操作 token 中携带了 tenantId
                        有 tenantId 说明是租户内的雇员在操作,tenantId = 1 是平台管理员在操作,
                        没有 tenantId 说明是用户在操作
                        """)
                .defaultTools(toolCallbackProvider)
                .build();
    }
}

修改源码

主要在方法上添加注解,注意 name 有命名规范,不能是中文,最好类似 selectMenuIdsByRoleIds。

  • @Tool(name = "selectMenuIdsByRoleIds", description = "根据角色id列表查询菜单id列表")
    

还可以在字段,方法参数上添加

  • @ToolParam(description = "角色id列表")
    
    /**
     * 根据角色id查询菜单id
     *
     * @param roleIds 角色id
     * @return 菜单id, 平铺, 去重
     */
    @Tool(name = "selectMenuIdsByRoleIds", description = "根据角色id列表查询菜单id列表")
    public List<Long> selectMenuIdsByRoleIds(@ToolParam(description = "角色id列表") List<Long> roleIds) {
        List<RoleMenuPo> poList = roleMenuRepository.findByRoleIdIn(roleIds);
        List<Long> menuIdList = poList.stream().map(RoleMenuPo::getMenuId).distinct().collect(Collectors.toList());
        log.info("根据角色id查询菜单id, roleIds:{}, menuIdList:{}", roleIds, menuIdList);
        return menuIdList;
    }

配置聊天接口

import com.quick.ai.pojo.dto.ChatRequest;
import com.quick.common.utils.lang.StringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;


import java.nio.charset.StandardCharsets;

/**
 * ai 对话
 *
 * @author wen7.online
 */
@Slf4j
@RestController
@RequestMapping(value = "/ai", name = "ai聊天")
public class AiController {
    @Resource
    private ChatClient chatClient;


    @PostMapping(value = "/v1/chat", name = "聊天")
    public String chat(@RequestBody ChatRequest chatRequest, HttpServletResponse response) {
        String userMessage = chatRequest.getMessage();
        log.info("用户问题 message:{}", userMessage);
        
        if (StringUtils.isEmpty(userMessage)) {
            return "";
        }
        
        String content = chatClient.prompt()
                .user(userMessage)
                .call()
                .content();
        return new String(content.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
    }
    
    //配置  produces = MediaType.TEXT_EVENT_STREAM_VALUE
    @PostMapping(value = "/v1/chat/stream", name = "聊天流式数据", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestBody ChatRequest chatRequest) {
        String userMessage = chatRequest.getMessage();

        Flux<String> flux = chatClient.prompt()
                .user(userMessage)
                .stream()
                .content();
        return flux;
    }

}

接口访问

调用接口

http://127.0.0.1:8080/ai/v1/chat
http://127.0.0.1:8080/ai/v1/chat/stream

前端代码 vue3

https://wen7.online/social/social_wechat

实现效果

通过自然语言实现,调用内部函数或接口,
虽然略有瑕疵,但是 领导说了,先上线吧,以后慢慢优化
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2332017.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【详细】MySQL 8 安装解压即用 (包含MySQL 5 卸载)

卸载MySQL 1.卸载 2.安装目录删除残余文件&#xff08;当初安装的位置&#xff09; 3.删除programData下面的mysql数据文件 4.检查mysql服务是否存在&#xff0c;如果存在则删除&#xff08;先暂停mysql服务&#xff09; sc delete mysql 5.删除注册表中残留信息 安装MySQL 8&…

显示器各类异常处理方法

显示器各类异常处理方法 导航 文章目录 显示器各类异常处理方法导航画面无显示/黑屏/无HDMI信号输入显示器闪烁显示器花屏显示画面模糊或扭曲显示器颜色异常显示器出现死点或亮点 画面无显示/黑屏/无HDMI信号输入 ​ 首先应该检查的是显示器电源&#xff08;真的有人弄掉电源…

一、简单的 Django 服务

一、配置虚拟环境 1.1 创建一个文件夹在导航栏输入cmd打开 1.2 安装依赖两个库 pip install virtualenv virtualenvwrapper-win -i https://pypi.tuna.tsinghua.edu.cn/simple验证是否安装成功 virtualenv --version pip show virtualenvwrapper-win 1.3 创建虚拟环境 mkvi…

k8s黑科技:Linux+Vagrant+VirtualBox开启Kubernetes奇幻之旅

文章目录 1. 准备硬件2. 安装系统3. 安装 VNC4. 基础配置4.1 路由转发4.2 防火墙4.3 selinux4.4 安装包4.5 重启 5. 配置代理6. 安装 virtuabox7. 安装 vagrant8. 配置 kubespray8.1 安装依赖工具8.2 定制 Vagrantfile8.3 配置代理与时间同步8.4 配置私有镜像仓库 9. 安装虚拟机…

34% 关税冲击下 LabVIEW 开发的变局

2025 年 4 月 4 日&#xff0c;中国国务院关税税则委员会宣布&#xff0c;自 4 月 10 日起对原产于美国的所有进口商品加征 34% 关税。这一举措&#xff0c;给 LabVIEW 开发领域带来显著影响&#xff0c;相关使用者和用户亟需采取应对策略。 ​ 从成本层面看&#xff0c;LabVI…

Http代理服务器选型与搭建

代理服务器选型-Squid 缓存加速 缓存频繁访问的网页、图片等静态资源&#xff0c;减少对原始服务器的重复请求&#xff0c;提升响应速度支持HTTP、HTTPS、FTP等协议&#xff0c;通过本地缓存直接响应客户端请求 访问控制 基于ACL&#xff08;访问控制列表&#xff09;实现精细…

Linux主要开发工具之gcc、gdb与make

此系列还有两篇&#xff0c;大家想完整掌握可以阅读另外两篇 Linux文本编辑与shell程序设计-CSDN博客 Linux基础知识详解与命令大全&#xff08;超详细&#xff09;-CSDN博客 1.gcc编译系统 1.1 文件名后缀 文件名后缀 文 件 类 型 文件名后缀 文 件 类 型 .c C源…

C++初阶-C++入门基础

目录 ​编辑 1.C的简介 1.1C的产生和发展 1.2C的参考文档 1.3C优势和难度 1.4C学习的建议 2.C的第一个程序 2.1打印Hello world 2.2头文件 2.3namespace命名空间 2.4&#xff1a;&#xff1a;作用域限定符 2.5namespace的延伸 2.6C的输入输出 3.总结 1.C的简介 …

idea手动创建resources文件夹

有时maven没有构建成功可能造成&#xff0c;resources文件夹不创建的现象 此时我们可以手动创建 手动创建

第十五届蓝桥杯大赛软件赛省赛Python 大学 C 组题目试做(中)【本期题目:回文数组,挖矿】

OK&#xff0c;继续写我们的第十五届蓝桥杯大赛软件赛省赛Python 大学 C 组题目&#xff0c;后面的题目比较麻烦了&#xff0c;所以我们再分两期讲。 这一期的题有 &#xff1a; 回文数组&#xff0c;挖矿 文章目录 回文数组基本思路第一步&#xff0c;获取半个数组每个数需要…

Qt动画 QAbstractAnimation

文章目录 简介QVariantAnimation 数值动画QPropertyAnimation 属性动画 QAnimationGroup 一组动画QParallelAnimationGroup 并行动画组QSequentialAnimationGroup 串行动画组 简介 QAbstractAnimation 是所有 Qt 动画的基类。 该类定义了所有动画应该都会有的功能函数。 要想实…

SpringMvc的请求-获得请求参数

客户端请求参数的格式是: namevalue&namevalue..… 服务器端要获得请求的参数&#xff0c;有时还需要进行数据的封装&#xff0c;SpringMVC可以接收如下类型的参数: 基本类型参数 POJO类型参数 数组类型参数 集合类型参数 获得基本类型参数 Controller中的业务方法…

flutter开发音乐APP(前提准备)

1、项目的一些环境&#xff1a; 2、接口文档&#xff1a; 酷狗音乐 NodeJS 版 API 3、接口数据结构化 Instantly parse JSON in any language | quicktype UI样式借鉴参考&#xff1a; Coffee-Expert/Apple-Music-New-UI: Apple Music Clone on Flutter, with redesigned UI…

使用docker搭建redis镜像时云服务器无法访问到国外的docker官网时如何解决

下载redis镜像 docker redis:版本号 此时截图中无法访问到国外的docker官网 解决方案&#xff1a; 通过更换镜像源来正常下载redis镜像 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<EOF {"registry-mirrors": ["https://docker.1…

双引擎驱动:解密音视频体验的QoS技术底座与QoE感官革命

QoS 定义&#xff1a;QoS&#xff08;Quality of Service&#xff0c;服务质量&#xff09;衡量音视频传输技术层面的性能表现&#xff0c;聚焦网络传输和系统处理能力&#xff0c;通过客观指标量化服务质量。核心指标 码率/带宽&#xff1a;数据传输速率上限&#xff0c;直接…

pom导包成功,但是就是无法使用相关类,同时报错:Library:Maven ‘xxx‘ has broken path

开发环境&#xff1a;Intellij 2023 一、问题记录 在maven工程的pom文件导入如下某一依赖(JGit)。没有显示导包的错误&#xff0c;同时在maven仓库里面找到对应的包是正常下载到相应jar的。 但是就是无法引入相关的类。打开Project Structure&#xff0c;在Dependencies中发现…

mysql的下载和安装2025.4.8

mysql下载和安装 MySQL的下载网址&#xff1a; https://www.mysql.com/downloads/ 点击进入Windows版本下载&#xff1a;我们可以选择需要的MySQL版本以及所需的操作系统&#xff0c;这里选择离线安装&#xff1a; 注意&#xff1a;MySQL 8.0 是带有 MySQL Installer 的最后一…

QML Loader:延迟加载与动态切换

目录 引言相关阅读工程结构LoaderDelay.qml - 延迟加载实现完整代码HeavyComponent.qml代码解析运行效果 LoaderSwitch.qml - 动态切换组件完整代码代码解析运行效果 Main.qml - 主界面实现完整代码主界面结构代码解析 总结下载链接 引言 QML的Loader组件提供了一种强大的机制…

Spark Core编程

一 Spark 运行架构 1 运行架构 定义 Spark 框架的核心是一个计算引擎&#xff0c;整体来说&#xff0c;它采用了标准 master-slave 的结构 如图所示 2 核心组件 Spark 框架有两个核心组件: 1)Driver 2)Spark 驱动器节点&#xff08;用于执行 Spark 任务中的 main 方法&…

无人机装调与测试

文章目录 前言一、无人机基本常识/预备知识&#xff08;一&#xff09;无人机飞行原理无人机硬件组成/各组件作用1.飞控2.GPS3.接收机4.电流计5.电调6.电机7.电池8.螺旋桨9.UBEC&#xff08;稳压模块&#xff09; &#xff08;二&#xff09;飞控硬件简介&#xff08;三&#x…