当前位置: 首页 > news >正文

SpringBoot 整合 MCP

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.dtcms.com/a/122226.html

相关文章:

  • 树莓派非桌面版无法ssh或vnc远程连接问题解决办法
  • 通过HTTP协议实现Git免密操作的解决方案
  • telophoto源码查看记录 三
  • 【回眸】Linux 内核 (十五) 之 多线程编程 上
  • 4月9日笔记
  • 2021-10-26 C++繁忙通信兵
  • Java 设计模式:原型模式详解
  • 使用雪花算法生成分布式唯一ID
  • Android 回答视频边播放边下载的问题
  • GMSL Strapping Pins CFG0/CFG1 应用
  • 【力扣刷题实战】外观数列
  • ragflow开启https访问:浏览器将自签证书添加到受信任的根证书颁发机构 ,当证书过期,还需要添加吗?
  • 第一部分——Docker篇 第六章 容器监控
  • vulnhub:sunset decoy
  • 洛谷普及B3691 [语言月赛202212] 狠狠地切割(Easy Version)
  • 优化 Web 性能:移除未使用的 CSS 规则(Unused CSS Rules)
  • The packaging for this project did not assign a file to the build artifact
  • 02.使用cline(VSCode插件)、continue(IDEA插件)、cherry-studio玩转MCP
  • Android里面开子线程的方法
  • OpenHarmony子系统开发 - 调测工具(二)
  • 柑橘病虫害图像分类数据集OrangeFruitDataset-8600
  • Python: 实现数据可视化分析系统
  • Coze平台 发布AI测试Agent的完整实现方案
  • redis_exporter服务安装并启动
  • STL-list链表
  • mac 苍穹外卖 后端初始 SkyApplication 报错
  • HTTP:一.概述
  • 【Leetcode-Hot100】移动零
  • 净室软件工程:以数学为基石的高可靠性软件开发之道
  • 数学建模--在新能源汽车研发测试中的革命性应用