SpringBoot 整合 MCP

06-01 1834阅读

SpringBoot 整合 MCP

MCP

MCP 协议主要分为:

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

    环境

    • JDK17
    • SpringBoot 3

      引入依赖

              
                  org.springframework.ai
                  spring-ai-core
                  1.0.0-M6
              
              
                  org.springframework.ai
                  spring-ai-spring-boot-autoconfigure
                  1.0.0-M6
              
              
                  org.springframework.ai
                  spring-ai-openai-spring-boot-starter
                  1.0.0-M6
              
              
                  org.springframework.ai
                  spring-ai-mcp-server-webmvc-spring-boot-starter
                  1.0.0-M6
              
      

      配置 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 findToolCallbackBeans() {
              String[] suffixes = {"Controller", "Service", "Manager"};
              String[] excludeNames = {"AiController"};		//这里是因为在 AiController 里循环引用了
              Set 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 toolObjects = springTools.findToolCallbackBeans().stream()
                      .map(springTools::unwrapProxy)  // 获取源对象,防止代理原因
                      .toList();
      		
              //核心,把所有的 bean 注入,会自动读取 @Tool 注解
              MethodToolCallbackProvider provider = MethodToolCallbackProvider.builder()
                      .toolObjects(toolObjects.toArray())
                      .build();
              List 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 selectMenuIdsByRoleIds(@ToolParam(description = "角色id列表") List roleIds) {
                  List poList = roleMenuRepository.findByRoleIdIn(roleIds);
                  List 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 chatStream(@RequestBody ChatRequest chatRequest) {
                  String userMessage = chatRequest.getMessage();
                  Flux 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
          

          实现效果

          通过自然语言实现,调用内部函数或接口,

          虽然略有瑕疵,但是 领导说了,先上线吧,以后慢慢优化

          SpringBoot 整合 MCP

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码