From f98c8bc665875e1e7795a332d8d0e7fd9e50fda1 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期四, 12 三月 2026 11:18:26 +0800
Subject: [PATCH] #
---
/dev/null | 30 ---
src/main/java/com/zy/ai/mcp/tool/WcsMcpTools.java | 73 +++++++
src/main/java/com/zy/ai/mcp/service/SpringAiMcpToolManager.java | 296 +++++++++++++++++++++++++++++
src/main/java/com/zy/common/config/CoolExceptionHandler.java | 3
src/main/java/com/zy/ai/mcp/config/SpringAiMcpTransportConfig.java | 113 +++++++++++
src/main/java/com/zy/common/config/AdminInterceptor.java | 3
src/main/java/com/zy/ai/service/WcsDiagnosisService.java | 35 ---
src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java | 17 +
pom.xml | 4
src/main/resources/application.yml | 20 ++
10 files changed, 532 insertions(+), 62 deletions(-)
diff --git a/pom.xml b/pom.xml
index dece7d5..0233f3c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -150,6 +150,10 @@
<artifactId>spring-ai-openai</artifactId>
</dependency>
<dependency>
+ <groupId>org.springframework.ai</groupId>
+ <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
+ </dependency>
+ <dependency>
<groupId>com.google.ortools</groupId>
<artifactId>ortools-java</artifactId>
<version>${ortools.version}</version>
diff --git a/src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java b/src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java
deleted file mode 100644
index c3ce29e..0000000
--- a/src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java
+++ /dev/null
@@ -1,232 +0,0 @@
-package com.zy.ai.mcp.config;
-
-import com.alibaba.fastjson.JSONObject;
-import com.zy.ai.mcp.dto.McpToolHandler;
-import com.zy.ai.mcp.dto.ToolDefinition;
-import com.zy.ai.mcp.dto.ToolRegistry;
-import com.zy.ai.mcp.service.WcsDataFacade;
-
-import java.util.*;
-
-public class McpToolsBootstrap {
-
- public static void registerAll(ToolRegistry registry, final WcsDataFacade facade) {
-
- registry.register(tool(
- "device_get_crn_status",
- "閫氳繃鍫嗗灈鏈虹紪鍙锋煡璇㈠爢鍨涙満璁惧瀹炴椂鏁版嵁",
- schemaObj(
- propArr("crnNos", true, "integer")
- ),
- schemaObj(
- propObj("devices", true)
- ),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getCrnDeviceStatus(args);
- }
- }
- ));
-
- registry.register(tool(
- "device_get_station_status",
- "鏌ヨ杈撻�佺嚎绔欑偣璁惧瀹炴椂鏁版嵁",
- schemaObj(
-
- ),
- schemaObj(
- propObj("stations", true)
- ),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getStationDeviceStatus(args);
- }
- }
- ));
-
- registry.register(tool(
- "device_get_rgv_status",
- "閫氳繃RGV缂栧彿鏌ヨRGV璁惧瀹炴椂鏁版嵁",
- schemaObj(
- propArr("rgvNos", true, "integer")
- ),
- schemaObj(
- propObj("devices", true)
- ),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getRgvDeviceStatus(args);
- }
- }
- ));
-
- registry.register(tool(
- "task_list",
- "閫氳繃绛涢�夋潯浠舵煡璇换鍔℃暟鎹�",
- schemaObj(
- propInt("crnNo", false),
- propInt("rgvNo", false),
- propArr("taskNos", false, "integer"),
- propInt("limit", false)
- ),
- schemaObj(propArr("tasks", true, "object")),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getTasks(args);
- }
- }
- ));
-
- registry.register(tool(
- "log_query",
- "閫氳繃绛涢�夋潯浠舵煡璇㈡棩蹇楁暟鎹�",
- schemaObj(
- propInt("limit", false)
- ),
- schemaObj(propArr("logs", true, "object")),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getLogs(args);
- }
- }
- ));
-
- registry.register(tool(
- "config_get_device_config",
- "閫氳繃璁惧缂栧彿鏌ヨ璁惧閰嶇疆鏁版嵁",
- schemaObj(
- propArr("crnNos", false, "integer"),
- propArr("rgvNos", false, "integer"),
- propArr("devpNos", false, "integer")
- ),
- schemaObj(propObj("deviceConfigs", true)),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getDeviceConfig(args);
- }
- }
- ));
-
- registry.register(tool(
- "config_get_system_config",
- "鏌ヨ绯荤粺閰嶇疆鏁版嵁",
- schemaObj(
-
- ),
- schemaObj(
- propObj("systemConfigs", true)
- ),
- new McpToolHandler() {
- public Object handle(JSONObject args) {
- return facade.getSystemConfig(args);
- }
- }
- ));
-
-// // 鈽� 璇婃柇鑱氬悎蹇収锛氫竴娆℃嬁鍏�
-// registry.register(tool(
-// "build_diagnosis_snapshot",
-// "Aggregate diagnosis snapshot: tasks + device realtime + configs + clipped logs for diagnosis.",
-// schemaObj(
-// propStr("warehouseCode", true),
-// propArr("deviceCodes", false, "string"), // 涓嶄紶鍒欐寜浠诲姟娑夊強璁惧鎺ㄥ
-// propStr("taskNo", false),
-// propStr("fromTime", true),
-// propStr("toTime", true),
-// propInt("taskLimit", false),
-// propInt("logLimit", false),
-// propArr("logKeywords", false, "string"),
-// propBool("includeSystemConfig", false),
-// propBool("includeDeviceConfig", false)
-// ),
-// schemaObj(
-// propObj("snapshot", true),
-// propArr("hints", false, "string")
-// ),
-// new McpToolHandler() {
-// public Object handle(JSONObject args) {
-// return facade.buildDiagnosisSnapshot(args);
-// }
-// }
-// ));
- }
-
- // ---------- schema helpers ----------
- private static ToolDefinition tool(String name, String desc,
- Map<String, Object> in, Map<String, Object> out,
- McpToolHandler handler) {
- ToolDefinition d = new ToolDefinition();
- d.setName(name);
- d.setDescription(desc);
- d.setInputSchema(in);
- d.setOutputSchema(out);
- d.setHandler(handler);
- return d;
- }
-
- private static Map<String, Object> schemaObj(Object... props) {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("type", "object");
-
- Map<String, Object> properties = new LinkedHashMap<String, Object>();
- List<String> required = new ArrayList<String>();
-
- for (Object p : props) {
- @SuppressWarnings("unchecked")
- Map<String, Object> pm = (Map<String, Object>) p;
- String name = String.valueOf(pm.get("name"));
- boolean req = Boolean.TRUE.equals(pm.get("required"));
- pm.remove("name");
- pm.remove("required");
- properties.put(name, pm);
- if (req) required.add(name);
- }
-
- m.put("properties", properties);
- if (!required.isEmpty()) m.put("required", required);
- return m;
- }
-
- private static Map<String, Object> propStr(String name, boolean required) {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("name", name);
- m.put("required", required);
- m.put("type", "string");
- return m;
- }
-
- private static Map<String, Object> propInt(String name, boolean required) {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("name", name);
- m.put("required", required);
- m.put("type", "integer");
- return m;
- }
-
- private static Map<String, Object> propBool(String name, boolean required) {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("name", name);
- m.put("required", required);
- m.put("type", "boolean");
- return m;
- }
-
- private static Map<String, Object> propObj(String name, boolean required) {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("name", name);
- m.put("required", required);
- m.put("type", "object");
- return m;
- }
-
- private static Map<String, Object> propArr(String name, boolean required, String itemType) {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("name", name);
- m.put("required", required);
- m.put("type", "array");
- Map<String, Object> items = new LinkedHashMap<String, Object>();
- items.put("type", itemType);
- m.put("items", items);
- return m;
- }
-}
diff --git a/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java b/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java
new file mode 100644
index 0000000..dc0aa9e
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/config/SpringAiMcpConfig.java
@@ -0,0 +1,17 @@
+package com.zy.ai.mcp.config;
+
+import com.zy.ai.mcp.tool.WcsMcpTools;
+import org.springframework.ai.support.ToolCallbacks;
+import org.springframework.ai.tool.StaticToolCallbackProvider;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class SpringAiMcpConfig {
+
+ @Bean("wcsMcpToolCallbackProvider")
+ public ToolCallbackProvider wcsMcpToolCallbackProvider(WcsMcpTools wcsMcpTools) {
+ return new StaticToolCallbackProvider(ToolCallbacks.from(wcsMcpTools));
+ }
+}
diff --git a/src/main/java/com/zy/ai/mcp/config/SpringAiMcpTransportConfig.java b/src/main/java/com/zy/ai/mcp/config/SpringAiMcpTransportConfig.java
new file mode 100644
index 0000000..6661926
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/config/SpringAiMcpTransportConfig.java
@@ -0,0 +1,113 @@
+package com.zy.ai.mcp.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;
+import io.modelcontextprotocol.server.McpServer;
+import io.modelcontextprotocol.server.McpServerFeatures;
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider;
+import io.modelcontextprotocol.spec.McpSchema;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerChangeNotificationProperties;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerProperties;
+import org.springframework.ai.mcp.server.common.autoconfigure.properties.McpServerSseProperties;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.web.context.support.StandardServletEnvironment;
+import org.springframework.web.servlet.function.RouterFunction;
+import org.springframework.web.servlet.function.ServerResponse;
+
+import java.util.Collections;
+import java.util.List;
+
+@Configuration
+@EnableConfigurationProperties(McpServerSseProperties.class)
+public class SpringAiMcpTransportConfig {
+
+ @Bean("wcsOfficialSseMcpSupport")
+ public OfficialSseMcpSupport wcsOfficialSseMcpSupport(
+ @Qualifier("mcpServerObjectMapper") ObjectMapper objectMapper,
+ McpServerSseProperties sseProperties,
+ McpServerProperties serverProperties,
+ McpServerChangeNotificationProperties changeNotificationProperties,
+ @Qualifier("syncTools") ObjectProvider<List<McpServerFeatures.SyncToolSpecification>> syncToolsProvider,
+ Environment environment) {
+
+ WebMvcSseServerTransportProvider transportProvider = WebMvcSseServerTransportProvider.builder()
+ .jsonMapper(new JacksonMcpJsonMapper(objectMapper))
+ .baseUrl(sseProperties.getBaseUrl())
+ .sseEndpoint(sseProperties.getSseEndpoint())
+ .messageEndpoint(sseProperties.getSseMessageEndpoint())
+ .keepAliveInterval(sseProperties.getKeepAliveInterval())
+ .build();
+
+ List<McpServerFeatures.SyncToolSpecification> syncTools = syncToolsProvider.getIfAvailable(Collections::emptyList);
+ McpSyncServer mcpSyncServer = buildSseSyncServer(
+ transportProvider,
+ serverProperties,
+ changeNotificationProperties,
+ syncTools,
+ environment
+ );
+
+ return new OfficialSseMcpSupport(transportProvider, mcpSyncServer);
+ }
+
+ @Bean("webMvcSseServerRouterFunction")
+ public RouterFunction<ServerResponse> webMvcSseServerRouterFunction(
+ @Qualifier("wcsOfficialSseMcpSupport") OfficialSseMcpSupport support) {
+ return support.routerFunction();
+ }
+
+ private McpSyncServer buildSseSyncServer(
+ WebMvcSseServerTransportProvider transportProvider,
+ McpServerProperties serverProperties,
+ McpServerChangeNotificationProperties changeNotificationProperties,
+ List<McpServerFeatures.SyncToolSpecification> syncTools,
+ Environment environment) {
+
+ McpServer.SingleSessionSyncSpecification specification = McpServer.sync(transportProvider);
+ specification.serverInfo(new McpSchema.Implementation(serverProperties.getName(), serverProperties.getVersion()));
+
+ McpSchema.ServerCapabilities.Builder capabilitiesBuilder = McpSchema.ServerCapabilities.builder();
+ if (serverProperties.getCapabilities().isTool()) {
+ capabilitiesBuilder.tools(changeNotificationProperties.isToolChangeNotification());
+ if (syncTools != null && !syncTools.isEmpty()) {
+ specification.tools(syncTools);
+ }
+ }
+
+ specification.capabilities(capabilitiesBuilder.build());
+ specification.instructions(serverProperties.getInstructions());
+ specification.requestTimeout(serverProperties.getRequestTimeout());
+ if (environment instanceof StandardServletEnvironment) {
+ specification.immediateExecution(true);
+ }
+
+ return specification.build();
+ }
+
+ public static final class OfficialSseMcpSupport implements AutoCloseable {
+
+ private final WebMvcSseServerTransportProvider transportProvider;
+ private final McpSyncServer mcpSyncServer;
+
+ public OfficialSseMcpSupport(WebMvcSseServerTransportProvider transportProvider, McpSyncServer mcpSyncServer) {
+ this.transportProvider = transportProvider;
+ this.mcpSyncServer = mcpSyncServer;
+ }
+
+ public RouterFunction<ServerResponse> routerFunction() {
+ return transportProvider.getRouterFunction();
+ }
+
+ @Override
+ public void close() {
+ mcpSyncServer.closeGracefully();
+ mcpSyncServer.close();
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/mcp/controller/McpController.java b/src/main/java/com/zy/ai/mcp/controller/McpController.java
deleted file mode 100644
index 3d8cf18..0000000
--- a/src/main/java/com/zy/ai/mcp/controller/McpController.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package com.zy.ai.mcp.controller;
-
-import com.alibaba.fastjson.JSON;
-import com.alibaba.fastjson.JSONObject;
-import com.zy.ai.mcp.config.McpToolsBootstrap;
-import com.zy.ai.mcp.dto.JsonRpcRequest;
-import com.zy.ai.mcp.dto.JsonRpcResponse;
-import com.zy.ai.mcp.dto.ToolDefinition;
-import com.zy.ai.mcp.dto.ToolRegistry;
-import com.zy.ai.mcp.service.WcsDataFacade;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
-
-import jakarta.annotation.PostConstruct;
-import java.util.*;
-
-@Slf4j
-@RestController
-@RequestMapping("/ai/mcp")
-public class McpController {
-
- private final ToolRegistry registry = new ToolRegistry();
-
- @Autowired
- private WcsDataFacade wcsDataFacade;
-
- public McpController(WcsDataFacade wcsDataFacade) {
- this.wcsDataFacade = wcsDataFacade;
- }
-
- @PostConstruct
- public void init() {
- McpToolsBootstrap.registerAll(registry, wcsDataFacade);
- log.info("MCP initialized, tools={}", registry.listTools().size());
- }
-
- @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
- public Object handle(@RequestBody JsonRpcRequest req,
- @RequestHeader(value = "Authorization", required = false) String auth) {
-
- // 锛堝缓璁級鍋氫竴涓畝鍗曢壌鏉冿細闃叉琚殢渚胯皟鐢ㄧ敓浜х郴缁�
- // if (!"Bearer your-token".equals(auth)) return JsonRpcResponse.err(null, 401, "Unauthorized", null);
-
- String id = req.getId();
- String method = req.getMethod();
- JSONObject params = JSON.parseObject(JSON.toJSONString(req.getParams()));
-
- try {
- if ("initialize".equals(method)) {
- Map<String, Object> result = new LinkedHashMap<String, Object>();
- result.put("serverName", "wcs-mcp");
- result.put("serverVersion", "1.0.0");
- result.put("capabilities", Arrays.asList("tools"));
- return JsonRpcResponse.ok(id, result);
- }
-
- if ("tools/list".equals(method)) {
- Map<String, Object> result = new LinkedHashMap<String, Object>();
- result.put("tools", registry.listTools());
- // cursor/paging 浣犲悗闈㈤渶瑕佸啀鍔�
- return JsonRpcResponse.ok(id, result);
- }
-
- if ("tools/call".equals(method)) {
- String toolName = params.getString("name");
- JSONObject arguments = params.getJSONObject("arguments");
- if (toolName == null || toolName.trim().isEmpty()) {
- return JsonRpcResponse.err(id, -32602, "Invalid params: missing tool name", null);
- }
- ToolDefinition def = registry.get(toolName);
- if (def == null) {
- return JsonRpcResponse.err(id, -32601, "Method not found: tool " + toolName, null);
- }
- Object output = def.getHandler().handle(arguments == null ? new JSONObject() : arguments);
-
- Map<String, Object> result = new LinkedHashMap<String, Object>();
- result.put("content", output); // 浣犱篃鍙互鎸� MCP 甯歌杩斿洖缁撴瀯鍋� text/json 鍒嗘
- return JsonRpcResponse.ok(id, result);
- }
-
- return JsonRpcResponse.err(id, -32601, "Method not found: " + method, null);
-
- } catch (Exception e) {
- log.error("MCP handle error, method={}, params={}", method, params, e);
- return JsonRpcResponse.err(id, -32000, "Server error", e.getMessage());
- }
- }
-
- public List<Map<String, Object>> listTools() {
- return registry.listTools();
- }
-
- public Object callTool(String toolName, JSONObject arguments) throws Exception {
- if (toolName == null || toolName.trim().isEmpty()) {
- throw new IllegalArgumentException("missing tool name");
- }
- ToolDefinition def = registry.get(toolName);
- if (def == null) {
- throw new IllegalArgumentException("tool not found: " + toolName);
- }
- return def.getHandler().handle(arguments == null ? new JSONObject() : arguments);
- }
-}
diff --git a/src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java b/src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java
deleted file mode 100644
index 30da47b..0000000
--- a/src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.zy.ai.mcp.dto;
-
-import lombok.Data;
-
-@Data
-class JsonRpcError {
- private int code;
- private String message;
- private Object data;
-
- public JsonRpcError(int code, String message, Object data) {
- this.code = code;
- this.message = message;
- this.data = data;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java b/src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java
deleted file mode 100644
index ee49c4e..0000000
--- a/src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.zy.ai.mcp.dto;
-
-import lombok.Data;
-
-import java.util.Map;
-
-@Data
-public class JsonRpcRequest {
- private String jsonrpc; // "2.0"
- private String id; // string/number 閮借锛岃繖閲岀敤 string
- private String method; // "tools/list" | "tools/call"
- private Map<String, Object> params;
-}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java b/src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java
deleted file mode 100644
index 7759350..0000000
--- a/src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.zy.ai.mcp.dto;
-
-import lombok.Data;
-
-@Data
-public class JsonRpcResponse {
- private String jsonrpc = "2.0";
- private String id;
- private Object result;
- private JsonRpcError error;
-
- public static JsonRpcResponse ok(String id, Object result) {
- JsonRpcResponse r = new JsonRpcResponse();
- r.id = id;
- r.result = result;
- return r;
- }
-
- public static JsonRpcResponse err(String id, int code, String message, Object data) {
- JsonRpcResponse r = new JsonRpcResponse();
- r.id = id;
- r.error = new JsonRpcError(code, message, data);
- return r;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java b/src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java
deleted file mode 100644
index 0671298..0000000
--- a/src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.zy.ai.mcp.dto;
-
-import com.alibaba.fastjson.JSONObject;
-
-public interface McpToolHandler {
- Object handle(JSONObject arguments) throws Exception;
-}
diff --git a/src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java b/src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java
deleted file mode 100644
index ead355f..0000000
--- a/src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.zy.ai.mcp.dto;
-
-
-import lombok.Data;
-
-import java.util.*;
-
-@Data
-public class ToolDefinition {
- private String name;
- private String description;
- private Map<String, Object> inputSchema; // JSON Schema as Map
- private Map<String, Object> outputSchema; // JSON Schema as Map
- private McpToolHandler handler;
-
- public Map<String, Object> toMcpToolJson() {
- Map<String, Object> m = new LinkedHashMap<String, Object>();
- m.put("name", name);
- m.put("description", description);
- m.put("inputSchema", inputSchema);
- m.put("outputSchema", outputSchema);
- return m;
- }
-}
-
diff --git a/src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java b/src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java
deleted file mode 100644
index 1f44742..0000000
--- a/src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.zy.ai.mcp.dto;
-
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-
-public class ToolRegistry {
- private final Map<String, ToolDefinition> tools = new ConcurrentHashMap<String, ToolDefinition>();
-
- public void register(ToolDefinition def) {
- tools.put(def.getName(), def);
- }
-
- public List<Map<String, Object>> listTools() {
- List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
- for (ToolDefinition def : tools.values()) {
- list.add(def.toMcpToolJson());
- }
- // 涓轰簡绋冲畾杈撳嚭锛屾寜 name 鎺掑簭
- Collections.sort(list, new Comparator<Map<String, Object>>() {
- public int compare(Map<String, Object> a, Map<String, Object> b) {
- return String.valueOf(a.get("name")).compareTo(String.valueOf(b.get("name")));
- }
- });
- return list;
- }
-
- public ToolDefinition get(String name) {
- return tools.get(name);
- }
-}
diff --git a/src/main/java/com/zy/ai/mcp/service/SpringAiMcpToolManager.java b/src/main/java/com/zy/ai/mcp/service/SpringAiMcpToolManager.java
new file mode 100644
index 0000000..a40a717
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/service/SpringAiMcpToolManager.java
@@ -0,0 +1,296 @@
+package com.zy.ai.mcp.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import io.modelcontextprotocol.client.McpClient;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.ToolCallbackProvider;
+import org.springframework.ai.tool.definition.ToolDefinition;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import jakarta.annotation.PreDestroy;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class SpringAiMcpToolManager {
+
+ private static final McpSchema.Implementation CLIENT_INFO =
+ new McpSchema.Implementation("wcs-ai-assistant", "1.0.0");
+
+ private final Object clientMonitor = new Object();
+
+ @Value("${server.port:9090}")
+ private Integer serverPort;
+
+ @Value("${server.servlet.context-path:}")
+ private String contextPath;
+
+ @Value("${spring.ai.mcp.server.sse-endpoint:/ai/mcp/sse}")
+ private String sseEndpoint;
+
+ @Value("${spring.ai.mcp.server.request-timeout:20s}")
+ private Duration requestTimeout;
+
+ @Value("${app.ai.mcp.client.base-url:}")
+ private String configuredBaseUrl;
+
+ private volatile ClientSession clientSession;
+
+ public List<Map<String, Object>> listTools() {
+ List<Map<String, Object>> tools = new ArrayList<Map<String, Object>>();
+ for (ToolCallback callback : getToolCallbacks()) {
+ if (callback == null || callback.getToolDefinition() == null) {
+ continue;
+ }
+ ToolDefinition definition = callback.getToolDefinition();
+ Map<String, Object> item = new LinkedHashMap<String, Object>();
+ item.put("name", definition.name());
+ item.put("description", definition.description());
+ item.put("inputSchema", parseSchema(definition.inputSchema()));
+ tools.add(item);
+ }
+ tools.sort(new Comparator<Map<String, Object>>() {
+ @Override
+ public int compare(Map<String, Object> left, Map<String, Object> right) {
+ return String.valueOf(left.get("name")).compareTo(String.valueOf(right.get("name")));
+ }
+ });
+ return tools;
+ }
+
+ public List<Object> buildOpenAiTools() {
+ List<Object> tools = new ArrayList<Object>();
+ for (Map<String, Object> item : listTools()) {
+ Object name = item.get("name");
+ if (name == null) {
+ continue;
+ }
+
+ Map<String, Object> function = new LinkedHashMap<String, Object>();
+ function.put("name", String.valueOf(name));
+ Object description = item.get("description");
+ if (description != null) {
+ function.put("description", String.valueOf(description));
+ }
+ Object inputSchema = item.get("inputSchema");
+ function.put("parameters", inputSchema == null ? new LinkedHashMap<String, Object>() : inputSchema);
+
+ Map<String, Object> tool = new LinkedHashMap<String, Object>();
+ tool.put("type", "function");
+ tool.put("function", function);
+ tools.add(tool);
+ }
+ return tools;
+ }
+
+ public Object callTool(String toolName, JSONObject arguments) {
+ if (toolName == null || toolName.trim().isEmpty()) {
+ throw new IllegalArgumentException("missing tool name");
+ }
+
+ ToolCallback callback = findCallback(toolName);
+ if (callback == null) {
+ throw new IllegalArgumentException("tool not found: " + toolName);
+ }
+
+ String rawResult = callback.call(arguments == null ? "{}" : arguments.toJSONString());
+ return parseToolResult(rawResult);
+ }
+
+ private ToolCallback findCallback(String toolName) {
+ for (ToolCallback callback : getToolCallbacks()) {
+ if (callback == null || callback.getToolDefinition() == null) {
+ continue;
+ }
+ if (toolName.equals(callback.getToolDefinition().name())) {
+ return callback;
+ }
+ }
+ return null;
+ }
+
+ private ToolCallback[] getToolCallbacks() {
+ try {
+ ToolCallback[] callbacks = ensureToolCallbackProvider().getToolCallbacks();
+ return callbacks == null ? new ToolCallback[0] : callbacks;
+ } catch (Exception e) {
+ log.warn("Failed to load MCP tools through SSE client, baseUrl={}, sseEndpoint={}",
+ resolveBaseUrl(), resolveClientSseEndpoint(), e);
+ resetClientSession();
+ return new ToolCallback[0];
+ }
+ }
+
+ private Object parseToolResult(String rawResult) {
+ if (rawResult == null || rawResult.trim().isEmpty()) {
+ return rawResult;
+ }
+ try {
+ return JSON.parse(rawResult);
+ } catch (Exception ignore) {
+ return rawResult;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> parseSchema(String inputSchema) {
+ if (inputSchema == null || inputSchema.trim().isEmpty()) {
+ return Collections.emptyMap();
+ }
+ try {
+ Object parsed = JSON.parse(inputSchema);
+ if (parsed instanceof Map) {
+ return new LinkedHashMap<String, Object>((Map<String, Object>) parsed);
+ }
+ } catch (Exception e) {
+ log.warn("Failed to parse MCP tool schema: {}", inputSchema, e);
+ }
+ Map<String, Object> fallback = new LinkedHashMap<String, Object>();
+ fallback.put("type", "object");
+ return fallback;
+ }
+
+ private ToolCallbackProvider ensureToolCallbackProvider() {
+ return ensureClientSession().toolCallbackProvider;
+ }
+
+ private ClientSession ensureClientSession() {
+ ClientSession current = clientSession;
+ if (current != null) {
+ return current;
+ }
+
+ synchronized (clientMonitor) {
+ current = clientSession;
+ if (current != null) {
+ return current;
+ }
+
+ String baseUrl = resolveBaseUrl();
+ String clientSseEndpoint = resolveClientSseEndpoint();
+ HttpClientSseClientTransport transport = HttpClientSseClientTransport.builder(baseUrl)
+ .sseEndpoint(clientSseEndpoint)
+ .connectTimeout(requestTimeout)
+ .build();
+
+ McpSyncClient syncClient = McpClient.sync(transport)
+ .clientInfo(CLIENT_INFO)
+ .requestTimeout(requestTimeout)
+ .initializationTimeout(requestTimeout)
+ .build();
+ syncClient.initialize();
+
+ SyncMcpToolCallbackProvider callbackProvider = new SyncMcpToolCallbackProvider(syncClient);
+ current = new ClientSession(syncClient, callbackProvider, baseUrl);
+ clientSession = current;
+ log.info("Spring AI MCP SSE client initialized, baseUrl={}, sseEndpoint={}, tools={}",
+ baseUrl, clientSseEndpoint, current.toolCallbackProvider.getToolCallbacks().length);
+ return current;
+ }
+ }
+
+ private void resetClientSession() {
+ synchronized (clientMonitor) {
+ ClientSession current = clientSession;
+ clientSession = null;
+ if (current != null) {
+ current.close();
+ }
+ }
+ }
+
+ private String resolveBaseUrl() {
+ if (configuredBaseUrl != null && !configuredBaseUrl.trim().isEmpty()) {
+ return trimTrailingSlash(configuredBaseUrl.trim());
+ }
+ StringBuilder url = new StringBuilder("http://127.0.0.1:");
+ url.append(serverPort == null ? 9090 : serverPort);
+ return trimTrailingSlash(url.toString());
+ }
+
+ private String resolveClientSseEndpoint() {
+ String endpoint = normalizePath(sseEndpoint);
+ if (configuredBaseUrl != null && !configuredBaseUrl.trim().isEmpty()) {
+ return endpoint;
+ }
+ String context = normalizeContextPath(contextPath);
+ if (context.isEmpty()) {
+ return endpoint;
+ }
+ return context + endpoint;
+ }
+
+ private String normalizeContextPath(String path) {
+ if (path == null || path.trim().isEmpty() || "/".equals(path.trim())) {
+ return "";
+ }
+ String value = path.trim();
+ if (!value.startsWith("/")) {
+ value = "/" + value;
+ }
+ return trimTrailingSlash(value);
+ }
+
+ private String normalizePath(String path) {
+ if (path == null || path.trim().isEmpty()) {
+ return "/";
+ }
+ String value = path.trim();
+ if (!value.startsWith("/")) {
+ value = "/" + value;
+ }
+ return value;
+ }
+
+ private String trimTrailingSlash(String value) {
+ if (value == null || value.isEmpty()) {
+ return "";
+ }
+ return value.endsWith("/") && value.length() > 1 ? value.substring(0, value.length() - 1) : value;
+ }
+
+ @PreDestroy
+ public void destroy() {
+ resetClientSession();
+ }
+
+ private static final class ClientSession implements AutoCloseable {
+
+ private final McpSyncClient syncClient;
+ private final ToolCallbackProvider toolCallbackProvider;
+ private final String baseUrl;
+
+ private ClientSession(McpSyncClient syncClient, ToolCallbackProvider toolCallbackProvider, String baseUrl) {
+ this.syncClient = syncClient;
+ this.toolCallbackProvider = toolCallbackProvider;
+ this.baseUrl = baseUrl;
+ }
+
+ @Override
+ public void close() {
+ try {
+ syncClient.closeGracefully();
+ } catch (Exception e) {
+ log.debug("Close MCP SSE client gracefully failed, baseUrl={}", baseUrl, e);
+ }
+ try {
+ syncClient.close();
+ } catch (Exception e) {
+ log.debug("Close MCP SSE client failed, baseUrl={}", baseUrl, e);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/zy/ai/mcp/tool/WcsMcpTools.java b/src/main/java/com/zy/ai/mcp/tool/WcsMcpTools.java
new file mode 100644
index 0000000..ebcd402
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/tool/WcsMcpTools.java
@@ -0,0 +1,73 @@
+package com.zy.ai.mcp.tool;
+
+import com.alibaba.fastjson.JSONObject;
+import com.zy.ai.mcp.service.WcsDataFacade;
+import lombok.RequiredArgsConstructor;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.ai.tool.annotation.ToolParam;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Component
+@RequiredArgsConstructor
+public class WcsMcpTools {
+
+ private final WcsDataFacade wcsDataFacade;
+
+ @Tool(name = "device_get_crn_status", description = "閫氳繃鍫嗗灈鏈虹紪鍙锋煡璇㈠爢鍨涙満璁惧瀹炴椂鏁版嵁")
+ public Object getCrnDeviceStatus(
+ @ToolParam(description = "鍫嗗灈鏈虹紪鍙峰垪琛紝涓嶄紶鍒欐煡璇㈠叏閮ㄥ爢鍨涙満", required = false) List<Integer> crnNos) {
+ return wcsDataFacade.getCrnDeviceStatus(json().fluentPut("crnNos", crnNos));
+ }
+
+ @Tool(name = "device_get_station_status", description = "鏌ヨ杈撻�佺嚎绔欑偣璁惧瀹炴椂鏁版嵁")
+ public Object getStationDeviceStatus() {
+ return wcsDataFacade.getStationDeviceStatus(json());
+ }
+
+ @Tool(name = "device_get_rgv_status", description = "閫氳繃RGV缂栧彿鏌ヨRGV璁惧瀹炴椂鏁版嵁")
+ public Object getRgvDeviceStatus(
+ @ToolParam(description = "RGV缂栧彿鍒楄〃锛屼笉浼犲垯鏌ヨ鍏ㄩ儴RGV", required = false) List<Integer> rgvNos) {
+ return wcsDataFacade.getRgvDeviceStatus(json().fluentPut("rgvNos", rgvNos));
+ }
+
+ @Tool(name = "task_list", description = "閫氳繃绛涢�夋潯浠舵煡璇换鍔℃暟鎹�")
+ public Object getTasks(
+ @ToolParam(description = "鍫嗗灈鏈虹紪鍙�", required = false) Integer crnNo,
+ @ToolParam(description = "RGV缂栧彿", required = false) Integer rgvNo,
+ @ToolParam(description = "浠诲姟鍗曞彿鍒楄〃", required = false) List<Integer> taskNos,
+ @ToolParam(description = "杩斿洖鏉℃暟涓婇檺锛岄粯璁� 200", required = false) Integer limit) {
+ return wcsDataFacade.getTasks(json()
+ .fluentPut("crnNo", crnNo)
+ .fluentPut("rgvNo", rgvNo)
+ .fluentPut("taskNos", taskNos)
+ .fluentPut("limit", limit));
+ }
+
+ @Tool(name = "log_query", description = "閫氳繃绛涢�夋潯浠舵煡璇� AI 鏃ュ織鏁版嵁")
+ public Object getLogs(
+ @ToolParam(description = "杩斿洖鏃ュ織琛屾暟涓婇檺锛岄粯璁� 500", required = false) Integer limit) {
+ return wcsDataFacade.getLogs(json().fluentPut("limit", limit));
+ }
+
+ @Tool(name = "config_get_device_config", description = "閫氳繃璁惧缂栧彿鏌ヨ璁惧閰嶇疆鏁版嵁")
+ public Object getDeviceConfig(
+ @ToolParam(description = "鍫嗗灈鏈虹紪鍙峰垪琛�", required = false) List<Integer> crnNos,
+ @ToolParam(description = "RGV缂栧彿鍒楄〃", required = false) List<Integer> rgvNos,
+ @ToolParam(description = "杈撻�佺嚎缂栧彿鍒楄〃", required = false) List<Integer> devpNos) {
+ return wcsDataFacade.getDeviceConfig(json()
+ .fluentPut("crnNos", crnNos)
+ .fluentPut("rgvNos", rgvNos)
+ .fluentPut("devpNos", devpNos));
+ }
+
+ @Tool(name = "config_get_system_config", description = "鏌ヨ绯荤粺閰嶇疆鏁版嵁")
+ public Object getSystemConfig() {
+ return wcsDataFacade.getSystemConfig(json());
+ }
+
+ private JSONObject json() {
+ return new JSONObject();
+ }
+}
diff --git a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
index 45dea58..83f0380 100644
--- a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
+++ b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -5,7 +5,7 @@
import com.zy.ai.entity.ChatCompletionRequest;
import com.zy.ai.entity.ChatCompletionResponse;
import com.zy.ai.entity.WcsDiagnosisRequest;
-import com.zy.ai.mcp.controller.McpController;
+import com.zy.ai.mcp.service.SpringAiMcpToolManager;
import com.zy.ai.utils.AiPromptUtils;
import com.zy.ai.utils.AiUtils;
import com.zy.common.utils.RedisUtil;
@@ -36,7 +36,7 @@
@Autowired
private AiUtils aiUtils;
@Autowired(required = false)
- private McpController mcpController;
+ private SpringAiMcpToolManager mcpToolManager;
public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) {
List<ChatCompletionRequest.Message> messages = new ArrayList<>();
@@ -257,8 +257,8 @@
SseEmitter emitter,
String chatId) {
try {
- if (mcpController == null) return false;
- List<Object> tools = buildOpenAiTools();
+ if (mcpToolManager == null) return false;
+ List<Object> tools = mcpToolManager.buildOpenAiTools();
if (tools.isEmpty()) return false;
baseMessages.add(systemPrompt);
@@ -303,7 +303,7 @@
}
Object output;
try {
- output = mcpController.callTool(toolName, args);
+ output = mcpToolManager.callTool(toolName, args);
} catch (Exception e) {
java.util.LinkedHashMap<String, Object> err = new java.util.LinkedHashMap<String, Object>();
err.put("tool", toolName);
@@ -386,31 +386,6 @@
} catch (Exception e) {
log.warn("SSE send failed", e);
}
- }
-
- private List<Object> buildOpenAiTools() {
- if (mcpController == null) return java.util.Collections.emptyList();
- List<Map<String, Object>> mcpTools = mcpController.listTools();
- if (mcpTools == null || mcpTools.isEmpty()) return java.util.Collections.emptyList();
-
- List<Object> tools = new ArrayList<>();
- for (Map<String, Object> t : mcpTools) {
- if (t == null) continue;
- Object name = t.get("name");
- if (name == null) continue;
- Object inputSchema = t.get("inputSchema");
- java.util.LinkedHashMap<String, Object> function = new java.util.LinkedHashMap<String, Object>();
- function.put("name", String.valueOf(name));
- Object desc = t.get("description");
- if (desc != null) function.put("description", String.valueOf(desc));
- function.put("parameters", inputSchema == null ? new java.util.LinkedHashMap<String, Object>() : inputSchema);
-
- java.util.LinkedHashMap<String, Object> tool = new java.util.LinkedHashMap<String, Object>();
- tool.put("type", "function");
- tool.put("function", function);
- tools.add(tool);
- }
- return tools;
}
private void sendLargeText(SseEmitter emitter, String text) {
diff --git a/src/main/java/com/zy/common/config/AdminInterceptor.java b/src/main/java/com/zy/common/config/AdminInterceptor.java
index a9badbf..f2b9668 100644
--- a/src/main/java/com/zy/common/config/AdminInterceptor.java
+++ b/src/main/java/com/zy/common/config/AdminInterceptor.java
@@ -64,6 +64,9 @@
}
// 璺ㄥ煙璁剧疆
// response.setHeader("Access-Control-Allow-Origin", "*");
+ if (!(handler instanceof HandlerMethod)) {
+ return true;
+ }
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
if (method.isAnnotationPresent(ManagerAuth.class)){
diff --git a/src/main/java/com/zy/common/config/CoolExceptionHandler.java b/src/main/java/com/zy/common/config/CoolExceptionHandler.java
index 79cfc08..c33ca24 100644
--- a/src/main/java/com/zy/common/config/CoolExceptionHandler.java
+++ b/src/main/java/com/zy/common/config/CoolExceptionHandler.java
@@ -7,7 +7,6 @@
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
-import org.springframework.web.method.HandlerMethod;
/**
* Created by vincent on 2019-06-09
@@ -19,7 +18,7 @@
private I18nMessageService i18nMessageService;
@ExceptionHandler(Exception.class)
- public R handlerException(HandlerMethod handler, Exception e) {
+ public R handlerException(Exception e) {
e.printStackTrace();
return R.error(i18nMessageService.getMessage("response.common.systemError"));
}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 6c3a42f..99d3d5e 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -51,6 +51,26 @@
await-termination-period: 30s
lifecycle:
timeout-per-shutdown-phase: 20s
+ ai:
+ mcp:
+ server:
+ base-url: "${app.ai.mcp.server.public-base-url:http://127.0.0.1:${server.port:9090}${server.servlet.context-path:}}"
+ name: wcs-mcp
+ version: 1.0.0
+ protocol: STREAMABLE
+ type: SYNC
+ sse-endpoint: /ai/mcp/sse
+ sse-message-endpoint: /ai/mcp/message
+ streamable-http:
+ mcp-endpoint: /ai/mcp
+ instructions: 鎻愪緵 WCS 璁惧鐘舵�併�佷换鍔°�佹棩蹇楀拰閰嶇疆鏌ヨ鑳藉姏
+ annotation-scanner:
+ enabled: false
+ capabilities:
+ tool: true
+ resource: false
+ prompt: false
+ completion: false
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
--
Gitblit v1.9.1