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
实现效果
通过自然语言实现,调用内部函数或接口,
虽然略有瑕疵,但是 领导说了,先上线吧,以后慢慢优化