diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java index 58af2ba22..911a9a090 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java @@ -188,6 +188,9 @@ public class SecurityConfig { .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)) diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/pom.xml b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/pom.xml new file mode 100644 index 000000000..5817a07df --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + org.jeecgframework.boot + jeecg-boot-parent + 3.8.2 + ../../pom.xml + + + jeecg-module-mcp-server + + + 17 + 17 + UTF-8 + + + + + + + org.jeecgframework.boot + jeecg-system-biz + ${jeecgboot.version} + + + + org.springframework.ai + spring-ai-starter-mcp-server-webmvc + + + + \ No newline at end of file diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/LlmToolsTestController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/LlmToolsTestController.java new file mode 100644 index 000000000..74c837648 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/LlmToolsTestController.java @@ -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 arguments = JSONObject.parseObject(toolExecutionRequest.arguments()); + String username = arguments.get("username").toString(); + List 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); + } + + +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/McpConfiguration.java b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/McpConfiguration.java new file mode 100644 index 000000000..5fb81717b --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/McpConfiguration.java @@ -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(); + } +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/UserMcpTool.java b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/UserMcpTool.java new file mode 100644 index 000000000..b97f3886a --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-mcp-server/src/main/java/org/jeecg/modules/mcp/UserMcpTool.java @@ -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 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 "创建部门成功"; + } + +} diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml b/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml index e5f50200d..b21355a0e 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml @@ -24,6 +24,12 @@ jeecg-module-demo ${jeecgboot.version} + + + org.jeecgframework.boot + jeecg-module-mcp-server + ${jeecgboot.version} + + + org.springframework.ai + spring-ai-bom + 1.0.1 + pom + import + @@ -481,7 +489,7 @@ org.jeecgframework.boot jeecg-boot-starter3-chatgpt - ${jeecgboot.version} + 3.8.3