Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ public class WebSocketConfig {
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
// @Bean
// public ServerEndpointExporter serverEndpointExporter() {
// return new ServerEndpointExporter();
// }

@Bean
public WebsocketFilter websocketFilter(){
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,110 @@
package org.jeecg.config.security;

import io.undertow.servlet.spec.HttpServletRequestImpl;
import io.undertow.util.HttpString;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;

/**
* 复制仪盘表请求query体携带的token
* @author eightmonth
* @date 2024/7/3 14:04
* 注意:改为容器无关实现,避免 Undertow 专有类型转换导致在 Tomcat 下 ClassCastException。
*
* 来源优先级:
* 1. Authorization 头(若存在则规范为 Bearer <token> 格式)
* 2. 查询参数 token
* 3. 自定义头 X-Access-Token
*
* 若最终获得 token,则通过请求包装器注入 Authorization 头,保持对下游过滤器/安全链可见。
*/
@Component
@Order(value = Integer.MIN_VALUE)
public class CopyTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 以下为undertow定制代码,如切换其它servlet容器,需要同步更换
HttpServletRequestImpl undertowRequest = (HttpServletRequestImpl) request;
String token = request.getHeader("Authorization");
if (StringUtils.hasText(token)) {
undertowRequest.getExchange().getRequestHeaders().remove("Authorization");
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + token);
// 容器无关实现:根据 header/参数提取 token,并以 Authorization 注入
String tokenHeader = request.getHeader("Authorization");
String candidate = null;
if (StringUtils.hasText(tokenHeader)) {
String trimmed = tokenHeader.trim();
if (startsWithIgnoreCase(trimmed, "Bearer ")) {
candidate = trimmed;
} else if (!trimmed.contains(" ")) { // 纯 token,无空格,视为需要规范化
candidate = trimmed;
} // 其他认证方案(如 Basic ...)保持不处理
} else {
String bearerToken = request.getParameter("token");
String headerBearerToken = request.getHeader("X-Access-Token");
if (StringUtils.hasText(bearerToken)) {
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + bearerToken);
candidate = bearerToken.trim();
} else if (StringUtils.hasText(headerBearerToken)) {
undertowRequest.getExchange().getRequestHeaders().add(new HttpString("Authorization"), "bearer " + headerBearerToken);
candidate = headerBearerToken.trim();
}
}
filterChain.doFilter(undertowRequest, response);

if (StringUtils.hasText(candidate)) {
final String authValue = startsWithIgnoreCase(candidate, "Bearer ") ? candidate : ("Bearer " + candidate);
HttpServletRequest wrapped = new AuthorizationHeaderRequestWrapper(request, authValue);
filterChain.doFilter(wrapped, response);
return;
}

filterChain.doFilter(request, response);
}

private boolean startsWithIgnoreCase(String str, String prefix) {
if (str == null || prefix == null) {
return false;
}
if (prefix.length() > str.length()) {
return false;
}
return str.regionMatches(true, 0, prefix, 0, prefix.length());
}

private static class AuthorizationHeaderRequestWrapper extends HttpServletRequestWrapper {
private final String authorization;

AuthorizationHeaderRequestWrapper(HttpServletRequest request, String authorization) {
super(request);
this.authorization = authorization;
}

@Override
public String getHeader(String name) {
if ("Authorization".equalsIgnoreCase(name)) {
return authorization;
}
return super.getHeader(name);
}

@Override
public Enumeration<String> getHeaders(String name) {
if ("Authorization".equalsIgnoreCase(name)) {
return Collections.enumeration(Collections.singletonList(authorization));
}
return super.getHeaders(name);
}

@Override
public Enumeration<String> getHeaderNames() {
Set<String> names = new LinkedHashSet<>();
Enumeration<String> e = super.getHeaderNames();
while (e.hasMoreElements()) {
names.add(e.nextElement());
}
names.add("Authorization");
return Collections.enumeration(names);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
.requestMatchers(AntPathRequestMatcher.antMatcher("/openapi/call/**")).permitAll()
// APP版本信息
.requestMatchers(AntPathRequestMatcher.antMatcher("/sys/version/app3version")).permitAll()
// mcp接口
.requestMatchers(AntPathRequestMatcher.antMatcher("/sse")).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/mcp/message")).permitAll()
.anyRequest().authenticated()
)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
@Slf4j
@Component
@ServerEndpoint("/vxeSocket/{userId}/{pageId}")
//@ServerEndpoint("/vxeSocket/{userId}/{pageId}")
public class VxeSocket {

/**
Expand Down
36 changes: 36 additions & 0 deletions jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-parent</artifactId>
<version>3.8.2</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>jeecg-module-mcp-server</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>


<dependencies>
<!-- SYSTEM 系统管理模块 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-biz</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
<!-- MCP-Server -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package org.jeecg.modules.mcp;

import com.alibaba.fastjson.JSONObject;
import dev.langchain4j.agent.tool.ToolSpecification;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.McpTransport;
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
import dev.langchain4j.model.chat.request.json.JsonStringSchema;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolExecutor;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.ai.assistant.AiChatAssistant;
import org.jeecg.ai.factory.AiModelFactory;
import org.jeecg.ai.factory.AiModelOptions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

/**
* @Description: llm工具调用测试接口
* @Author: chenrui
* @Date: 2025/8/22 14:13
*/
@RestController
@RequestMapping("/ai/tools/test")
@Slf4j
public class LlmToolsTestController {


@Autowired
ISysBaseAPI sysBaseAPI;

// 根据环境构建模型配置;缺少关键项则返回 null 以便测试跳过
private static AiModelOptions buildModelOptionsFromEnv() {
String baseUrl = "https://api.gpt.ge";
String apiKey = "sk-ZLhvUUGPGyERkPya632f3f18209946F7A51d4479081a3dFb";
String modelName = "gpt-4.1-mini";
return AiModelOptions.builder()
.provider(AiModelFactory.AIMODEL_TYPE_OPENAI)
.modelName(modelName)
.baseUrl(baseUrl)
.apiKey(apiKey)
.build();
}


@GetMapping(value = "/queryUser")
public Result<?> queryUser(@RequestParam(value = "prompt", required = true) String prompt) {
AiModelOptions modelOp = buildModelOptionsFromEnv();
ToolSpecification toolSpecification = ToolSpecification.builder()
.name("query_user_by_name")
.description("通过通过用户名查询用户信息,返回用户信息列表")
.parameters(JsonObjectSchema.builder()
.addProperties(Map.of(
"username", JsonStringSchema.builder()
.description("用户名,多个可以使用逗号分隔")
.build()
))
.build())
.build();
ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> {
Map<String, Object> arguments = JSONObject.parseObject(toolExecutionRequest.arguments());
String username = arguments.get("username").toString();
List<JSONObject> users = sysBaseAPI.queryUsersByUsernames(username);
return JSONObject.toJSONString(users);
};

// 构建同步聊天模型并通过 AiChatAssistant 发起一次非流式对话
ChatModel chatModel = AiModelFactory.createChatModel(modelOp);
AiChatAssistant bot = AiServices.builder(AiChatAssistant.class)
.chatModel(chatModel)
.tools(Map.of(toolSpecification, toolExecutor))
.tools()
.build();
String chat = bot.chat(prompt);
log.info("聊天回复: " + chat);
return Result.OK(chat);
}

// 根据环境构建 MCP 工具提供者;缺少 URL 则返回 null 以便测试跳过
private static McpToolProvider buildMcpTool(String sseUrl) {
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl(sseUrl)
.logRequests(true)
.logResponses(true)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.build();
return McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
}

/**
* 测试JeecgMcp
* @author chenrui
* @date 2025/8/22 10:10
*/
@GetMapping(value = "/queryUserByMcp")
public Result<?> testJeecgToolProvider(@RequestParam(value = "prompt", required = true) String prompt) {
// prompt = "查询一下有没有用户名是admin的用户信息";
// prompt = "新建一个顶级部门,部门名称是:测试部门,机构类别是公司";
AiModelOptions modelOp = buildModelOptionsFromEnv();
McpToolProvider toolProvider = buildMcpTool("http://localhost:8080/jeecgboot/sse");

// 构建同步聊天模型并通过 AiChatAssistant 发起一次非流式对话
ChatModel chatModel = AiModelFactory.createChatModel(modelOp);
AiChatAssistant bot = AiServices.builder(AiChatAssistant.class)
.chatModel(chatModel)
.toolProvider(toolProvider)
.build();
String chat = bot.chat(prompt);
log.info("聊天回复: " + chat);
return Result.OK(chat);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.jeecg.modules.mcp;

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;

/**
* @Description:
* @Author: chenrui
* @Date: 2025/8/21 17:19
*/
@Configuration
public class McpConfiguration {

@Bean
public ToolCallbackProvider weatherTools(UserMcpTool userMcpTool) {
return MethodToolCallbackProvider.builder().toolObjects(userMcpTool).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.jeecg.modules.mcp;

import com.alibaba.fastjson.JSONObject;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.system.entity.SysDepart;
import org.jeecg.modules.system.service.ISysDepartService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* @Description:
* @Author: chenrui
* @Date: 2025/8/21 17:10
*/
@Service("userMcpService")
public class UserMcpTool {

@Autowired
ISysBaseAPI sysBaseAPI;

@Autowired
ISysDepartService sysDepartmentService;

@Tool(name = "query_user_by_name", description = "通过通过用户名查询用户信息,返回用户信息列表")
public String queryUserByUsername(@ToolParam(description = "用户名,多个可以使用逗号分隔", required = true) String username) {
if (username == null || username.isEmpty()) {
return "Username cannot be null or empty";
}
List<JSONObject> users = sysBaseAPI.queryUsersByUsernames(username);
return JSONObject.toJSONString(users);
}

@Tool(name = "add_depart", description = "新增部门信息,返回操作结果")
public String saveDepartData(@ToolParam(description = "部门信息,departName(部门名称,必填),orgCategory(机构类别,必填, 1=公司,2=组织机构,3=岗位),parentId(父部门:父级部门的id,非必填)", required = true) SysDepart sysDepart){
try {
sysDepartmentService.saveDepartData(sysDepart, "mcpService");
} catch (Exception e) {
return "创建部门失败,原因:"+e.getMessage();
}
return "创建部门成功";
}

}
Loading