package com.iot.gateway.mcp; import com.fasterxml.jackson.databind.JsonNode; import com.iot.gateway.common.BackendInternalClient; import com.iot.gateway.common.BizException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Slf4j @Component public class McpToolRegistry { @Autowired private BackendInternalClient internalClient; /** * 演示/开发模式开关。生产环境下必须为 false,避免将后端故障误报为“成功”或演示响应 */ @Value("${gateway.mock.enabled:false}") private boolean mockEnabled; public List> getToolDefinitions() { List> tools = new ArrayList<>(); // 1. iot_device_list Map t1 = new HashMap<>(); t1.put("name", "iot_device_list"); t1.put("description", "查询当前租户名下的所有工业物联网设备台账与实时运行状态(在线/报警/停机)。所需权限范围: device.read"); Map s1 = new HashMap<>(); s1.put("type", "object"); Map p1 = new HashMap<>(); p1.put("status", Map.of("type", "string", "description", "设备状态过滤: online/offline/alarm,默认全部")); p1.put("pageNum", Map.of("type", "integer", "description", "分页页码,默认 1")); p1.put("pageSize", Map.of("type", "integer", "description", "每页条数,默认 10")); s1.put("properties", p1); t1.put("inputSchema", s1); tools.add(t1); // 2. iot_telemetry_latest Map t2 = new HashMap<>(); t2.put("name", "iot_telemetry_latest"); t2.put("description", "召测指定工业设备的最新实时点位遥测数据(注塑机温度、注塑压力、螺杆转速、能耗电流等)。所需权限范围: telemetry.read"); Map s2 = new HashMap<>(); s2.put("type", "object"); Map p2 = new HashMap<>(); p2.put("deviceId", Map.of("type", "string", "description", "设备唯一标识编码或编号")); s2.put("properties", p2); s2.put("required", List.of("deviceId")); t2.put("inputSchema", s2); tools.add(t2); // 3. iot_device_control Map t3 = new HashMap<>(); t3.put("name", "iot_device_control"); t3.put("description", "向边缘物联设备安全下发反写控制指令(如设定注塑温度、设备启停控制、工艺参数微调)。所需权限范围: device.control"); Map s3 = new HashMap<>(); s3.put("type", "object"); Map p3 = new HashMap<>(); p3.put("deviceId", Map.of("type", "string", "description", "目标设备编码")); p3.put("command", Map.of("type", "string", "description", "指令动作: start/stop/reboot/set_param")); p3.put("params", Map.of("type", "object", "description", "指令参数键值对")); s3.put("properties", p3); s3.put("required", List.of("deviceId", "command")); t3.put("inputSchema", s3); tools.add(t3); // 4. iot_alarm_query Map t4 = new HashMap<>(); t4.put("name", "iot_alarm_query"); t4.put("description", "按设备与时间范围检索未处理的设备故障与历史严重告警记录。所需权限范围: alarm.read"); Map s4 = new HashMap<>(); s4.put("type", "object"); Map p4 = new HashMap<>(); p4.put("deviceId", Map.of("type", "string", "description", "设备编码(可选)")); p4.put("severity", Map.of("type", "integer", "description", "严重级别: 1提示, 2次要, 3严重, 4紧急")); s4.put("properties", p4); t4.put("inputSchema", s4); tools.add(t4); return tools; } /** * 执行 MCP 工具调用,严格校验租户上下文与授权范围 Scope */ public Map executeTool(String toolName, Map args, Long tenantId, List scopes, boolean isAdmin) { log.info("执行 MCP 工具调用: tool={}, tenantId={}, args={}, scopes={}, isAdmin={}", toolName, tenantId, args, scopes, isAdmin); // 1. 严格租户隔离校验 if (tenantId == null) { throw new BizException(401, "MCP 工具执行拒绝: 缺少有效的租户上下文,严禁匿名或跨租户执行"); } // 2. 细粒度 Scope 校验 checkToolScope(toolName, scopes, isAdmin); Map result = new HashMap<>(); if ("iot_device_list".equals(toolName)) { Map params = new HashMap<>(); if (args != null && args.containsKey("status")) { String st = String.valueOf(args.get("status")); if ("online".equalsIgnoreCase(st)) params.put("online", "1"); else if ("offline".equalsIgnoreCase(st)) params.put("online", "0"); } if (args != null && args.containsKey("pageNum")) params.put("pageNum", String.valueOf(args.get("pageNum"))); if (args != null && args.containsKey("pageSize")) params.put("pageSize", String.valueOf(args.get("pageSize"))); // 专线调用核心业务后端多租户独立数据库 JsonNode resp = internalClient.get("/api/internal/open/devices", tenantId, params); if (resp != null && resp.has("code") && resp.get("code").asInt() == 200 && resp.has("data")) { JsonNode data = resp.get("data"); result.put("tenantId", tenantId); result.put("total", data.has("total") ? data.get("total").asLong() : 0); result.put("devices", data.has("records") ? data.get("records") : List.of()); return result; } // 非 mock 模式下直接明确报错 if (!mockEnabled) { String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "核心物联后端服务不可用或查询超时"; throw new BizException(502, "召测设备列表失败: " + errMsg); } // 开发演示兜底 result.put("tenantId", tenantId); result.put("isMock", true); result.put("total", 2); result.put("devices", List.of( Map.of("deviceCode", "DEV20261009001", "deviceName", "1号注塑机 (海天MA1200)", "online", 1), Map.of("deviceCode", "DEV20261009002", "deviceName", "2号注塑机 (海天MA2000)", "online", 0) )); } else if ("iot_telemetry_latest".equals(toolName)) { String deviceId = args != null && args.containsKey("deviceId") ? String.valueOf(args.get("deviceId")) : ""; if (deviceId.isEmpty()) { throw new BizException(400, "缺少必填参数: deviceId"); } JsonNode resp = internalClient.get("/api/internal/open/telemetry/latest", tenantId, Map.of("deviceId", deviceId)); if (resp != null && resp.has("code") && resp.get("code").asInt() == 200 && resp.has("data")) { result.put("tenantId", tenantId); result.put("deviceId", deviceId); result.put("data", resp.get("data")); return result; } if (!mockEnabled) { String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "后端遥测接口不可用或设备无数据"; throw new BizException(502, "召测设备遥测失败: " + errMsg); } result.put("isMock", true); result.put("deviceId", deviceId); result.put("timestamp", System.currentTimeMillis()); result.put("telemetry", Map.of( "barrel_temp_zone1", 215.4, "barrel_temp_zone2", 220.1, "injection_pressure_mpa", 13.8, "screw_speed_rpm", 85.0 )); } else if ("iot_device_control".equals(toolName)) { // 控制指令下发:绝不能在后端失败时误报“EXECUTED” JsonNode resp = internalClient.post("/api/internal/open/commands", tenantId, args); if (resp != null && resp.has("code") && resp.get("code").asInt() == 200 && resp.has("data")) { result.put("tenantId", tenantId); result.put("success", true); result.put("status", "EXECUTED"); result.put("data", resp.get("data")); return result; } String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "后端指令网关通信超时或未响应"; if (!mockEnabled) { log.error("下发设备控制指令失败: tool={}, tenantId={}, args={}, err={}", toolName, tenantId, args, errMsg); throw new BizException(502, "控制指令执行失败: " + errMsg); } result.put("isMock", true); result.put("success", false); result.put("status", "MOCK_MODE_REFUSED"); result.put("message", "当前为开发演示模式,已拒绝向物理设备真实下发控制指令: " + errMsg); } else if ("iot_alarm_query".equals(toolName)) { Map params = new HashMap<>(); if (args != null && args.containsKey("deviceId")) params.put("deviceId", String.valueOf(args.get("deviceId"))); if (args != null && args.containsKey("severity")) params.put("severity", String.valueOf(args.get("severity"))); JsonNode resp = internalClient.get("/api/internal/open/alarms", tenantId, params); if (resp != null && resp.has("code") && resp.get("code").asInt() == 200 && resp.has("data")) { result.put("tenantId", tenantId); result.put("alarms", resp.get("data")); return result; } if (!mockEnabled) { String errMsg = resp != null && resp.has("message") ? resp.get("message").asText() : "后端告警接口不可用"; throw new BizException(502, "查询告警失败: " + errMsg); } result.put("isMock", true); result.put("alarms", List.of( Map.of("alarmId", "ALM-2026100901", "deviceId", "DEV20261009001", "severity", 3, "title", "模具温度异常过高") )); } else { throw new BizException(400, "未知的 MCP 工具名称: " + toolName); } return result; } private void checkToolScope(String toolName, List scopes, boolean isAdmin) { if (isAdmin) return; String requiredScope; switch (toolName) { case "iot_device_list" -> requiredScope = "device.read"; case "iot_telemetry_latest" -> requiredScope = "telemetry.read"; case "iot_device_control" -> requiredScope = "device.control"; case "iot_alarm_query" -> requiredScope = "alarm.read"; default -> requiredScope = "all"; } if (scopes == null || scopes.isEmpty()) { throw new BizException(403, "权限不足: 客户端未具备任何授权范围,无法调用 [" + toolName + "]"); } String normReq = requiredScope.replace(":", "."); boolean match = false; for (String s : scopes) { String norm = s.trim().replace(":", "."); if (norm.equals(normReq) || norm.equals("all") || norm.equals("*")) { match = true; break; } } if (!match) { throw new BizException(403, "权限不足: 调用工具 [" + toolName + "] 需要具备 [" + requiredScope + "] 授权范围"); } } }