SpringBoot 整合 MCP
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
实现效果
通过自然语言实现,调用内部函数或接口,
虽然略有瑕疵,但是 领导说了,先上线吧,以后慢慢优化
-
-
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。