Copilot commented on code in PR #7893: URL: https://github.com/apache/incubator-seata/pull/7893#discussion_r2644559195
########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); Review Comment: The pathParams map is reassigned locally but this assignment has no effect outside the method since Java passes object references by value. If pathParams is null, the new HashMap created here won't be visible to the caller. Consider returning the pathParams map or requiring it to be non-null as a parameter. ```suggestion if (pathParams != null) { pathParams.put("vGroup", nameSpaceDetail.getvGroup()); } ``` ########## console/src/main/java/org/apache/seata/mcp/core/utils/DateUtils.java: ########## @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.core.utils; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.regex.Pattern; + +public class DateUtils { + + public static final Long ONE_DAY_TIMESTAMP = 86400000L; + + private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"); + + public static boolean isValidDate(String dateStr) { + return DATE_PATTERN.matcher(dateStr).matches(); + } + + public static long convertToTimestampFromDate(String dateStr) { + if (!isValidDate(dateStr)) { + throw new DateTimeException("The time format does not match yyyy-mm-dd"); + } + LocalDate date = LocalDate.parse(dateStr); + ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); + return zonedDateTime.toInstant().toEpochMilli(); + } + + public static long convertToTimeStampFromDateTime(String dateTimeStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + try { + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, formatter); + return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } catch (Exception e) { Review Comment: The method returns -1 on any exception during date parsing. This can lead to confusion as -1 is a valid timestamp (representing a time before the Unix epoch). Consider throwing a specific exception or returning an Optional to clearly indicate parsing failure rather than returning a potentially misleading value. ```suggestion } catch (DateTimeParseException e) { ``` ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + Review Comment: This method overrides [MCPRPCService.getCallNameSpace](1); it is advisable to add an Override annotation. ```suggestion @Override ``` ########## console/src/main/java/org/apache/seata/mcp/service/impl/ModifyConfirmServiceImpl.java: ########## @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class ModifyConfirmServiceImpl implements ModifyConfirmService { + + private static final Map<String, Long> MODIFY_KEY = new ConcurrentHashMap<>(); + + private static final long EXPIRE_MS = 60_000; // Key timeout period + + @Override + public Map<String, String> confirmAndGetKey() { + String key = UUID.randomUUID().toString(); + MODIFY_KEY.put(key, System.currentTimeMillis()); + Map<String, String> map = new HashMap<>(); + map.put("modify_key", key); + map.put("expire_time", "60s"); Review Comment: The hardcoded string "60s" should be derived from the EXPIRE_MS constant to keep them synchronized. If EXPIRE_MS changes, this message will become inaccurate. Consider calculating this dynamically (e.g., EXPIRE_MS / 1000 + "s") to ensure consistency. ```suggestion map.put("expire_time", EXPIRE_MS / 1000 + "s"); ``` ########## console/src/main/java/org/apache/seata/mcp/service/impl/ModifyConfirmServiceImpl.java: ########## @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class ModifyConfirmServiceImpl implements ModifyConfirmService { + + private static final Map<String, Long> MODIFY_KEY = new ConcurrentHashMap<>(); + + private static final long EXPIRE_MS = 60_000; // Key timeout period Review Comment: The magic number 60_000 for key expiration should be extracted as a named constant or made configurable via application properties. This would improve maintainability and allow administrators to adjust the timeout without code changes. ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); Review Comment: Creating a new RestTemplate instance as a field can lead to issues in production environments. RestTemplate should typically be configured as a Spring bean with proper connection pooling, timeouts, and error handling. Consider injecting RestTemplate as a dependency or using WebClient for better async support. ```suggestion public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils, RestTemplate restTemplate) { this.jwtTokenUtils = jwtTokenUtils; this.restTemplate = restTemplate; } private final RestTemplate restTemplate; ``` ########## console/src/main/java/org/apache/seata/mcp/core/utils/DateUtils.java: ########## @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.core.utils; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.regex.Pattern; + +public class DateUtils { + + public static final Long ONE_DAY_TIMESTAMP = 86400000L; + + private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"); + + public static boolean isValidDate(String dateStr) { + return DATE_PATTERN.matcher(dateStr).matches(); + } + + public static long convertToTimestampFromDate(String dateStr) { + if (!isValidDate(dateStr)) { + throw new DateTimeException("The time format does not match yyyy-mm-dd"); + } + LocalDate date = LocalDate.parse(dateStr); + ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); + return zonedDateTime.toInstant().toEpochMilli(); + } + + public static long convertToTimeStampFromDateTime(String dateTimeStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + try { + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, formatter); + return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } catch (Exception e) { + return -1; + } + } + + public static String convertToDateTimeFromTimestamp(Long timestamp) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime dateTime; + try { + dateTime = Instant.ofEpochMilli(timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } catch (DateTimeParseException e) { Review Comment: The method catches DateTimeParseException but Instant.ofEpochMilli() can throw DateTimeException (parent class) or ArithmeticException for invalid values. The current catch block will not handle these exceptions properly. Consider catching a broader exception type or handling all potential exceptions from timestamp conversion. ```suggestion } catch (DateTimeException | ArithmeticException e) { ``` ########## console/src/main/java/org/apache/seata/mcp/core/utils/DateUtils.java: ########## @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.core.utils; + +import java.time.DateTimeException; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.regex.Pattern; + +public class DateUtils { + + public static final Long ONE_DAY_TIMESTAMP = 86400000L; + + private static final Pattern DATE_PATTERN = Pattern.compile("^\\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$"); + + public static boolean isValidDate(String dateStr) { + return DATE_PATTERN.matcher(dateStr).matches(); + } + + public static long convertToTimestampFromDate(String dateStr) { + if (!isValidDate(dateStr)) { + throw new DateTimeException("The time format does not match yyyy-mm-dd"); + } + LocalDate date = LocalDate.parse(dateStr); + ZonedDateTime zonedDateTime = date.atStartOfDay(ZoneId.systemDefault()); + return zonedDateTime.toInstant().toEpochMilli(); + } + + public static long convertToTimeStampFromDateTime(String dateTimeStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + try { + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr, formatter); + return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } catch (Exception e) { + return -1; + } + } + + public static String convertToDateTimeFromTimestamp(Long timestamp) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime dateTime; + try { + dateTime = Instant.ofEpochMilli(timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + } catch (DateTimeParseException e) { + return "Parse Failed, please check that the timestamp is correct"; + } + return dateTime.format(formatter); + } + + public static boolean judgeExceedTimeDuration(Long startTime, Long endTime, Long maxDuration) { + if (endTime < startTime) return false; Review Comment: The logic returns false when endTime is less than startTime, which means the method will indicate the duration is valid (not exceeded) even when the time range is inverted. This is likely incorrect behavior. Consider throwing an exception or returning true (exceeded) when the time range is invalid to prevent silent failures. ```suggestion if (endTime < startTime) { throw new IllegalArgumentException("endTime must not be earlier than startTime"); } ``` ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + + public String getCallNameSpace( + String path, Object queryParams, Map<String, String> pathParams, HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call NameSpace Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String getCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call TC Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String deleteCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP DELETE request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP DELETE Call TC Failed: {}", e.getMessage()); + return responseBody; Review Comment: When a RestClientException is caught, the method returns responseBody which will always be null at this point since the exception occurred before or during the exchange. This results in returning null instead of providing meaningful error information. Consider returning an error message or throwing a custom exception to properly communicate the failure to the caller. ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalSessionTools.java: ########## @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.tools; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.result.PageResult; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.core.model.GlobalStatus; +import org.apache.seata.mcp.core.constant.RPCConstant; +import org.apache.seata.mcp.core.props.MCPProperties; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.core.utils.DateUtils; +import org.apache.seata.mcp.entity.dto.GlobalSessionParamDto; +import org.apache.seata.mcp.entity.param.GlobalAbnormalSessionParam; +import org.apache.seata.mcp.entity.param.GlobalSessionParam; +import org.apache.seata.mcp.entity.vo.GlobalSessionVO; +import org.apache.seata.mcp.service.MCPRPCService; +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class GlobalSessionTools { + + private final MCPRPCService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ObjectMapper objectMapper; + + private final ModifyConfirmService modifyConfirmService; + + public GlobalSessionTools( + MCPRPCService mcpRPCService, + MCPProperties mcpProperties, + ObjectMapper objectMapper, + ModifyConfirmService modifyConfirmService) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.objectMapper = objectMapper; + this.modifyConfirmService = modifyConfirmService; + } + + private final List<Integer> exceptionStatus = new ArrayList<>(); + + @McpTool(description = "Query global transactions") + public PageResult<GlobalSessionVO> queryGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query parameter objects") GlobalSessionParamDto paramDto) { + GlobalSessionParam param = GlobalSessionParam.covertFromDtoParam(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + throw new IllegalArgumentException( + "The query time span is not allowed to exceed the max query duration : " + + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()) + " hour"); + } + } else { + param.setTimeEnd(param.getTimeStart() + DateUtils.ONE_DAY_TIMESTAMP); + } + } else { + param.setTimeEnd(null); + param.setTimeStart(null); + } + PageResult<GlobalSessionVO> pageResult; + String result = mcpRPCService.getCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/query", param, null, null); + try { + pageResult = objectMapper.readValue(result, new TypeReference<PageResult<GlobalSessionVO>>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + if (pageResult == null) { + return PageResult.failure("", "query global session failed"); + } else { + return pageResult; + } + } + + @McpTool(description = "Delete the global session, Get the modify key before you delete") + public String deleteGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.deleteCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/deleteGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("delete global session failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Stop the global session retry, Get the modify key before you stop") + public String stopGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/stopGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("stop global session retry failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Start the global session retry, Get the modify key before you start") + public String startGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/startGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("start the global session retry failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Send global session to commit or rollback to rm, Get the modify key before you send") + public String sendCommitOrRollback( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/sendCommitOrRollback", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("send global session to commit or rollback to rm failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool( + description = + "Change the global session status, Used to change transactions that are in a failed commit or rollback failed state to a retry state, Get the modify key before you change") + public String changeGlobalStatus( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/changeGlobalStatus", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("change the global session status failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Check out the abnormal transaction information,You can specify the time") + public List<GlobalSessionVO> getAbnormalSessions( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query Param") GlobalAbnormalSessionParam abnormalSessionParam) { + List<GlobalSessionVO> result = new ArrayList<>(); + GlobalSessionParamDto param = GlobalSessionParamDto.covertFromAbnormalParam(abnormalSessionParam); + param.setPageNum(1); + param.setPageSize(100); + if (exceptionStatus.isEmpty()) { + exceptionStatus.add(GlobalStatus.CommitFailed.getCode()); + exceptionStatus.add(GlobalStatus.TimeoutRollbackFailed.getCode()); + exceptionStatus.add(GlobalStatus.RollbackFailed.getCode()); + } + for (Integer status : exceptionStatus) { + param.setStatus(status); + List<GlobalSessionVO> datas = + queryGlobalSession(nameSpaceDetail, param).getData(); + if (datas != null && !datas.isEmpty()) { + for (GlobalSessionVO vo : datas) { + if (result.size() >= 200) { Review Comment: The magic number 200 is used as a hard-coded limit for the result size. This value should be extracted as a named constant (e.g., MAX_ABNORMAL_SESSIONS_RESULT_SIZE) at the class level to improve code maintainability and make it easier to adjust this limit in the future. ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalSessionTools.java: ########## @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.tools; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.result.PageResult; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.core.model.GlobalStatus; +import org.apache.seata.mcp.core.constant.RPCConstant; +import org.apache.seata.mcp.core.props.MCPProperties; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.core.utils.DateUtils; +import org.apache.seata.mcp.entity.dto.GlobalSessionParamDto; +import org.apache.seata.mcp.entity.param.GlobalAbnormalSessionParam; +import org.apache.seata.mcp.entity.param.GlobalSessionParam; +import org.apache.seata.mcp.entity.vo.GlobalSessionVO; +import org.apache.seata.mcp.service.MCPRPCService; +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class GlobalSessionTools { + + private final MCPRPCService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ObjectMapper objectMapper; + + private final ModifyConfirmService modifyConfirmService; + + public GlobalSessionTools( + MCPRPCService mcpRPCService, + MCPProperties mcpProperties, + ObjectMapper objectMapper, + ModifyConfirmService modifyConfirmService) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.objectMapper = objectMapper; + this.modifyConfirmService = modifyConfirmService; + } + + private final List<Integer> exceptionStatus = new ArrayList<>(); + + @McpTool(description = "Query global transactions") + public PageResult<GlobalSessionVO> queryGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query parameter objects") GlobalSessionParamDto paramDto) { + GlobalSessionParam param = GlobalSessionParam.covertFromDtoParam(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + throw new IllegalArgumentException( + "The query time span is not allowed to exceed the max query duration : " + + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()) + " hour"); + } + } else { + param.setTimeEnd(param.getTimeStart() + DateUtils.ONE_DAY_TIMESTAMP); + } + } else { + param.setTimeEnd(null); + param.setTimeStart(null); + } + PageResult<GlobalSessionVO> pageResult; + String result = mcpRPCService.getCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/query", param, null, null); + try { + pageResult = objectMapper.readValue(result, new TypeReference<PageResult<GlobalSessionVO>>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + if (pageResult == null) { + return PageResult.failure("", "query global session failed"); + } else { + return pageResult; + } + } + + @McpTool(description = "Delete the global session, Get the modify key before you delete") + public String deleteGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.deleteCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/deleteGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("delete global session failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Stop the global session retry, Get the modify key before you stop") + public String stopGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/stopGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("stop global session retry failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Start the global session retry, Get the modify key before you start") + public String startGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/startGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("start the global session retry failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Send global session to commit or rollback to rm, Get the modify key before you send") + public String sendCommitOrRollback( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/sendCommitOrRollback", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("send global session to commit or rollback to rm failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool( + description = + "Change the global session status, Used to change transactions that are in a failed commit or rollback failed state to a retry state, Get the modify key before you change") + public String changeGlobalStatus( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/changeGlobalStatus", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("change the global session status failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Check out the abnormal transaction information,You can specify the time") + public List<GlobalSessionVO> getAbnormalSessions( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query Param") GlobalAbnormalSessionParam abnormalSessionParam) { + List<GlobalSessionVO> result = new ArrayList<>(); + GlobalSessionParamDto param = GlobalSessionParamDto.covertFromAbnormalParam(abnormalSessionParam); + param.setPageNum(1); + param.setPageSize(100); + if (exceptionStatus.isEmpty()) { + exceptionStatus.add(GlobalStatus.CommitFailed.getCode()); + exceptionStatus.add(GlobalStatus.TimeoutRollbackFailed.getCode()); + exceptionStatus.add(GlobalStatus.RollbackFailed.getCode()); + } Review Comment: The exceptionStatus list is initialized in the getAbnormalSessions method on every call. This is inefficient as the list contains constant status codes that should be initialized once. Consider moving this initialization to a constructor or using a static initializer block to initialize the list only once when the class is loaded. ########## console/src/main/java/org/apache/seata/mcp/core/props/NameSpaceDetail.java: ########## @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.core.props; + +import org.apache.seata.common.util.StringUtils; + +public class NameSpaceDetail { + private String namespace; + private String cluster; + private String vGroup; + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public String getvGroup() { + return vGroup; + } + + public void setvGroup(String vGroup) { Review Comment: The method name getvGroup uses non-standard Java naming convention for getters. According to JavaBeans conventions, getter methods should follow camelCase pattern. This should be renamed to getVGroup (with capital V) to follow standard naming conventions and maintain consistency. ```suggestion public String getVGroup() { return vGroup; } public void setVGroup(String vGroup) { ``` ########## console/src/main/java/org/apache/seata/mcp/tools/ModifyConfirmTools.java: ########## @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.tools; + +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class ModifyConfirmTools { + + private final ModifyConfirmService modifyConfirmService; + + public ModifyConfirmTools(ModifyConfirmService modifyConfirmService) { + this.modifyConfirmService = modifyConfirmService; + } + + private static final Logger LOGGER = LoggerFactory.getLogger(ModifyConfirmTools.class); + Review Comment: The logger is declared after the constructor, which violates the typical Java style convention of declaring static final fields at the top of the class. Consider moving this logger declaration before the constructor to improve code readability and consistency with standard Java conventions. ```suggestion private static final Logger LOGGER = LoggerFactory.getLogger(ModifyConfirmTools.class); private final ModifyConfirmService modifyConfirmService; public ModifyConfirmTools(ModifyConfirmService modifyConfirmService) { this.modifyConfirmService = modifyConfirmService; } ``` ########## console/src/main/java/org/apache/seata/mcp/service/impl/ModifyConfirmServiceImpl.java: ########## @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class ModifyConfirmServiceImpl implements ModifyConfirmService { + + private static final Map<String, Long> MODIFY_KEY = new ConcurrentHashMap<>(); Review Comment: The MODIFY_KEY map can grow indefinitely if keys expire but are not removed. While isValidKey() removes expired keys when accessed, keys that are never accessed again will remain in memory forever. Consider implementing a background cleanup task or using a cache with TTL capabilities (like Caffeine or Guava Cache) to automatically evict expired entries. ########## console/src/main/java/org/apache/seata/mcp/core/props/NameSpaceDetail.java: ########## @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.core.props; + +import org.apache.seata.common.util.StringUtils; + +public class NameSpaceDetail { + private String namespace; + private String cluster; + private String vGroup; + + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + public String getCluster() { + return cluster; + } + + public void setCluster(String cluster) { + this.cluster = cluster; + } + + public String getvGroup() { + return vGroup; + } + + public void setvGroup(String vGroup) { Review Comment: The method name setvGroup uses non-standard Java naming convention for setters. According to JavaBeans conventions, setter methods should follow camelCase pattern. This should be renamed to setVGroup (with capital V) to follow standard naming conventions and maintain consistency. ```suggestion public String getVGroup() { return vGroup; } public void setVGroup(String vGroup) { ``` ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + + public String getCallNameSpace( + String path, Object queryParams, Map<String, String> pathParams, HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call NameSpace Failed: {}", e.getMessage()); + return responseBody; Review Comment: When a RestClientException is caught, the method returns responseBody which will always be null at this point since the exception occurred before or during the exchange. This results in returning null instead of providing meaningful error information. Consider returning an error message or throwing a custom exception to properly communicate the failure to the caller. ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + + public String getCallNameSpace( + String path, Object queryParams, Map<String, String> pathParams, HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call NameSpace Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String getCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call TC Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String deleteCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP DELETE request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP DELETE Call TC Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String putCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP PUT request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP Put Call TC Failed: {}", e.getMessage()); + return responseBody; Review Comment: When a RestClientException is caught, the method returns responseBody which will always be null at this point since the exception occurred before or during the exchange. This results in returning null instead of providing meaningful error information. Consider returning an error message or throwing a custom exception to properly communicate the failure to the caller. ```suggestion logger.error("MCP Put Call TC Failed", e); return "MCP Put Call TC Failed: " + e.getMessage(); ``` ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalSessionTools.java: ########## @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.tools; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.result.PageResult; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.core.model.GlobalStatus; +import org.apache.seata.mcp.core.constant.RPCConstant; +import org.apache.seata.mcp.core.props.MCPProperties; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.core.utils.DateUtils; +import org.apache.seata.mcp.entity.dto.GlobalSessionParamDto; +import org.apache.seata.mcp.entity.param.GlobalAbnormalSessionParam; +import org.apache.seata.mcp.entity.param.GlobalSessionParam; +import org.apache.seata.mcp.entity.vo.GlobalSessionVO; +import org.apache.seata.mcp.service.MCPRPCService; +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Service +public class GlobalSessionTools { + + private final MCPRPCService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ObjectMapper objectMapper; + + private final ModifyConfirmService modifyConfirmService; + + public GlobalSessionTools( + MCPRPCService mcpRPCService, + MCPProperties mcpProperties, + ObjectMapper objectMapper, + ModifyConfirmService modifyConfirmService) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.objectMapper = objectMapper; + this.modifyConfirmService = modifyConfirmService; + } + + private final List<Integer> exceptionStatus = new ArrayList<>(); + + @McpTool(description = "Query global transactions") + public PageResult<GlobalSessionVO> queryGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query parameter objects") GlobalSessionParamDto paramDto) { + GlobalSessionParam param = GlobalSessionParam.covertFromDtoParam(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + throw new IllegalArgumentException( + "The query time span is not allowed to exceed the max query duration : " + + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()) + " hour"); + } + } else { + param.setTimeEnd(param.getTimeStart() + DateUtils.ONE_DAY_TIMESTAMP); + } + } else { + param.setTimeEnd(null); + param.setTimeStart(null); + } + PageResult<GlobalSessionVO> pageResult; + String result = mcpRPCService.getCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/query", param, null, null); + try { + pageResult = objectMapper.readValue(result, new TypeReference<PageResult<GlobalSessionVO>>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + if (pageResult == null) { + return PageResult.failure("", "query global session failed"); + } else { + return pageResult; + } + } + + @McpTool(description = "Delete the global session, Get the modify key before you delete") + public String deleteGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.deleteCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/deleteGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("delete global session failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Stop the global session retry, Get the modify key before you stop") + public String stopGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/stopGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("stop global session retry failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Start the global session retry, Get the modify key before you start") + public String startGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/startGlobalSession", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("start the global session retry failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Send global session to commit or rollback to rm, Get the modify key before you send") + public String sendCommitOrRollback( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/sendCommitOrRollback", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("send global session to commit or rollback to rm failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool( + description = + "Change the global session status, Used to change transactions that are in a failed commit or rollback failed state to a retry state, Get the modify key before you change") + public String changeGlobalStatus( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global transaction id") String xid, + @McpToolParam(description = "Modify key") String modifyKey) { + if (!modifyConfirmService.isValidKey(modifyKey)) { + return "The modify key is not available"; + } + Map<String, String> pathParams = new HashMap<>(); + pathParams.put("xid", xid); + String result = mcpRPCService.putCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/changeGlobalStatus", null, pathParams, null); + if (StringUtils.isBlank(result)) { + return String.format("change the global session status failed, xid: %s", xid); + } else { + return result; + } + } + + @McpTool(description = "Check out the abnormal transaction information,You can specify the time") + public List<GlobalSessionVO> getAbnormalSessions( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query Param") GlobalAbnormalSessionParam abnormalSessionParam) { + List<GlobalSessionVO> result = new ArrayList<>(); + GlobalSessionParamDto param = GlobalSessionParamDto.covertFromAbnormalParam(abnormalSessionParam); + param.setPageNum(1); + param.setPageSize(100); Review Comment: The magic number 100 used for pageSize should be extracted as a named constant (e.g., DEFAULT_ABNORMAL_SESSION_PAGE_SIZE) at the class level. This improves code maintainability and makes the value's purpose clearer. ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; Review Comment: The hardcoded URL "http://127.0.0.1:%s" restricts the service to only communicate with localhost. This should be made configurable through application properties to support distributed deployments where TC nodes may be on different hosts. Consider injecting this as a configuration property. ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + + public String getCallNameSpace( + String path, Object queryParams, Map<String, String> pathParams, HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call NameSpace Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String getCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call TC Failed: {}", e.getMessage()); + return responseBody; Review Comment: When a RestClientException is caught, the method returns responseBody which will always be null at this point since the exception occurred before or during the exchange. This results in returning null instead of providing meaningful error information. Consider returning an error message or throwing a custom exception to properly communicate the failure to the caller. ########## console/src/main/java/org/apache/seata/mcp/service/impl/MCPRPCServiceImpl.java: ########## @@ -0,0 +1,283 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.seata.mcp.service.impl; + +import org.apache.seata.common.exception.AuthenticationFailedException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.console.config.WebSecurityConfig; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.MCPRPCService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@Service +public class MCPRPCServiceImpl implements MCPRPCService { + + private final JwtTokenUtils jwtTokenUtils; + + public MCPRPCServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + private final RestTemplate restTemplate = new RestTemplate(); + + private final String NAMING_SPACE_URL = "http://127.0.0.1:%s"; + + private final Logger logger = LoggerFactory.getLogger(MCPRPCServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + public String getToken() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) { + throw new AuthenticationFailedException("No right to be identified"); + } + String originJwt = (String) auth.getCredentials(); + if (!jwtTokenUtils.validateToken(originJwt)) { + throw new AuthenticationFailedException("Invalid token, please log back in to get a new token"); + } + return WebSecurityConfig.TOKEN_PREFIX + originJwt; + } + + public void setNamespaceHeaderAndPathParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> pathParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (pathParams == null) { + pathParams = new HashMap<>(); + } + pathParams.put("vGroup", nameSpaceDetail.getvGroup()); + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + + public String getCallNameSpace( + String path, Object queryParams, Map<String, String> pathParams, HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call NameSpace Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String getCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP GET request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP GET Call TC Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String deleteCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP DELETE request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP DELETE Call TC Failed: {}", e.getMessage()); + return responseBody; + } + } + + @Override + public String putCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object queryParams, + Map<String, String> pathParams, + HttpHeaders headers) { + if (headers == null) { + headers = new HttpHeaders(); + } + if (nameSpaceDetail == null || !nameSpaceDetail.isValid()) { + return "If you have not specified the namespace of the TC/Server, specify the namespace first"; + } else { + setNamespaceHeaderAndPathParam(nameSpaceDetail, headers, pathParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(queryParams); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, pathParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.PUT, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP PUT request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP Put Call TC Failed: {}", e.getMessage()); + return responseBody; + } + } + + private Map<String, Object> objectToQueryParamMap(Object obj) { + if (obj == null) { + return Collections.emptyMap(); + } + + Map<String, Object> paramMap = new HashMap<>(); + + if (obj instanceof Map) { + ((Map<?, ?>) obj).forEach((key, value) -> { + if (key != null && value != null) { + paramMap.put(key.toString(), value); + } + }); + return paramMap; + } + + Class<?> clazz = obj.getClass(); + for (Field field : clazz.getDeclaredFields()) { + try { + field.setAccessible(true); Review Comment: Using reflection with setAccessible(true) can cause security issues in environments with a SecurityManager and may fail with Java modules. Additionally, this approach bypasses encapsulation. Consider using a library like Jackson's ObjectMapper.convertValue() or defining a proper interface/contract for objects that need to be converted to query parameters. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
