Copilot commented on code in PR #7893: URL: https://github.com/apache/incubator-seata/pull/7893#discussion_r2646691445
########## console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java: ########## @@ -0,0 +1,241 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.NamingServerLocalMarker; +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.ConsoleApiService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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 java.util.Map; + +import static org.apache.seata.mcp.core.utils.UrlUtils.buildUrl; +import static org.apache.seata.mcp.core.utils.UrlUtils.objectToQueryParamMap; + +@ConditionalOnMissingBean(NamingServerLocalMarker.class) +@Service +public class ConsoleRemoteServiceImpl implements ConsoleApiService { + + private final JwtTokenUtils jwtTokenUtils; + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + private final AuthenticationManager authenticationManager; + + public ConsoleRemoteServiceImpl( + JwtTokenUtils jwtTokenUtils, + RestTemplate restTemplate, + ObjectMapper objectMapper, + AuthenticationManager authenticationManager) { + this.jwtTokenUtils = jwtTokenUtils; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + this.authenticationManager = authenticationManager; + } + + @Value("${seata.console.naming-space-url:http://127.0.0.1:%s}") + private String NAMING_SPACE_URL; Review Comment: The variable 'NAMING_SPACE_URL' does not follow Java naming conventions. Constants should be in SCREAMING_SNAKE_CASE, but this is actually an instance variable with the @Value annotation, not a compile-time constant. Instance variables should use camelCase naming. Rename this to 'namingSpaceUrl' to follow Java naming conventions. ```suggestion private String namingSpaceUrl; ``` ########## 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 static final Logger LOGGER = LoggerFactory.getLogger(ModifyConfirmTools.class); + + private final ModifyConfirmService modifyConfirmService; + + public ModifyConfirmTools(ModifyConfirmService modifyConfirmService) { + this.modifyConfirmService = modifyConfirmService; + } + + @McpTool( + description = "Before modifying (update or delete) a transaction or lock, the user MUST manually confirm." + + "You are NOT allowed to fabricate or auto-confirm on behalf of the user.") + public Map<String, String> confirmAndGetKey( + @McpToolParam( + description = + "The confirmation string provided by the USER (not generated by the LLM).The content must repeat the modification action clearly.") + String userInputStr) { + if (StringUtils.isBlank(userInputStr)) { + throw new IllegalArgumentException("User confirmation string is required."); + } + if (!userInputStr.contains("确认") && !userInputStr.contains("confirm")) { + throw new IllegalArgumentException( + "Confirmation string must explicitly contain '确认' or 'confirm' and repeat the modification content. This must come from the user."); + } Review Comment: The validation logic for user confirmation is weak and could be bypassed with minimal effort. The check only verifies that the input contains "确认" or "confirm" but doesn't validate that the user actually understood what they're confirming. A malicious actor or automated tool could easily provide a string that contains these words. Consider implementing a more robust confirmation mechanism, such as requiring the user to type the specific XID or operation details they're confirming. ########## console/src/main/java/org/apache/seata/mcp/entity/dto/McpGlobalSessionParamDto.java: ########## @@ -0,0 +1,142 @@ +/* + * 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.entity.dto; + +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.mcp.entity.param.McpGlobalAbnormalSessionParam; +import org.springaicommunity.mcp.annotation.McpToolParam; + +import java.io.Serializable; + +public class McpGlobalSessionParamDto implements Serializable { + + private static final long serialVersionUID = 115488252809011284L; + + @McpToolParam(description = "GLOBAL TRANSACTIONS id", required = false) + private String xid; + + @McpToolParam(description = "applicationId", required = false) + private String applicationId; + + @McpToolParam(description = "the state enumeration class is in example", required = false) + private Integer status; + + @McpToolParam(description = "The name of the transaction", required = false) + private String transactionName; + + @McpToolParam(description = "Whether or not it contains branch transaction information", required = false) + private boolean withBranch; + + @McpToolParam(description = "PAGE NUMBER") + private int pageNum; + + @McpToolParam(description = "PageSize") + private int pageSize; + + @McpToolParam(description = "The transaction start time is after this time (yyyy-MM-dd HH:mm:ss)", required = false) + private String timeStart; + + @McpToolParam( + description = "The transaction start time is before this time (yyyy-MM-dd HH:mm:ss)", + required = false) + private String timeEnd; + + public int getPageNum() { + return pageNum; + } + + public void setPageNum(int pageNum) { + this.pageNum = pageNum; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public String getTimeStart() { + return timeStart; + } + + public void setTimeStart(String timeStart) { + this.timeStart = timeStart; + } + + public String getTimeEnd() { + return timeEnd; + } + + public void setTimeEnd(String timeEnd) { + this.timeEnd = timeEnd; + } + + public String getXid() { + return xid; + } + + public void setXid(String xid) { + this.xid = xid; + } + + public String getTransactionName() { + return transactionName; + } + + public void setTransactionName(String transactionName) { + this.transactionName = transactionName; + } + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public boolean isWithBranch() { + return withBranch; + } + + public void setWithBranch(boolean withBranch) { + this.withBranch = withBranch; + } + + public static McpGlobalSessionParamDto covertFromAbnormalParam(McpGlobalAbnormalSessionParam abParam) { Review Comment: The method 'covertFromAbnormalParam' has a spelling error in its name. It should be 'convertFromAbnormalParam' (with an 'n'). This typo makes the API less professional and could cause confusion for developers using this method. ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalSessionTools.java: ########## @@ -0,0 +1,224 @@ +/* + * 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.McpGlobalSessionParamDto; +import org.apache.seata.mcp.entity.param.McpGlobalAbnormalSessionParam; +import org.apache.seata.mcp.entity.param.McpGlobalSessionParam; +import org.apache.seata.mcp.entity.vo.McpGlobalSessionVO; +import org.apache.seata.mcp.service.ConsoleApiService; +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 ConsoleApiService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ObjectMapper objectMapper; + + private final ModifyConfirmService modifyConfirmService; + + private final List<Integer> exceptionStatus = new ArrayList<>(); + + public static int ABNORMAL_SESSION_PAGE_SIZE = 30; + + public GlobalSessionTools( + ConsoleApiService mcpRPCService, + MCPProperties mcpProperties, + ObjectMapper objectMapper, + ModifyConfirmService modifyConfirmService) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.objectMapper = objectMapper; + this.modifyConfirmService = modifyConfirmService; + exceptionStatus.add(GlobalStatus.CommitFailed.getCode()); + exceptionStatus.add(GlobalStatus.TimeoutRollbackFailed.getCode()); + exceptionStatus.add(GlobalStatus.RollbackFailed.getCode()); + } + + @McpTool(description = "Query global transactions") + public PageResult<McpGlobalSessionVO> queryGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query parameter objects") McpGlobalSessionParamDto paramDto) { + McpGlobalSessionParam param = McpGlobalSessionParam.covertFromDtoParam(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + return PageResult.failure( + "", + String.format( + "The query time span is not allowed to exceed the max query duration: %s hour", + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()))); Review Comment: The error message returned when time validation fails includes only the hour value but refers to it generically as "max query duration". The message should be more specific about the time unit. Consider changing the message format to explicitly state "hours" in the message, e.g., "The query time span is not allowed to exceed %s hours". ########## console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java: ########## @@ -0,0 +1,241 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.NamingServerLocalMarker; +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.ConsoleApiService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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 java.util.Map; + +import static org.apache.seata.mcp.core.utils.UrlUtils.buildUrl; +import static org.apache.seata.mcp.core.utils.UrlUtils.objectToQueryParamMap; + +@ConditionalOnMissingBean(NamingServerLocalMarker.class) +@Service +public class ConsoleRemoteServiceImpl implements ConsoleApiService { + + private final JwtTokenUtils jwtTokenUtils; + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + private final AuthenticationManager authenticationManager; + + public ConsoleRemoteServiceImpl( + JwtTokenUtils jwtTokenUtils, + RestTemplate restTemplate, + ObjectMapper objectMapper, + AuthenticationManager authenticationManager) { + this.jwtTokenUtils = jwtTokenUtils; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + this.authenticationManager = authenticationManager; + } + + @Value("${seata.console.naming-space-url:http://127.0.0.1:%s}") + private String NAMING_SPACE_URL; + + private final Logger logger = LoggerFactory.getLogger(ConsoleRemoteServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + @Value("${seata.mcp.auth.enabled}") + private String enabledAuth; + + public String getToken() { + if (Boolean.parseBoolean(enabledAuth)) { + 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; + } else { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken("seata", ""); + Authentication authentication = authenticationManager.authenticate(authenticationToken); + return WebSecurityConfig.TOKEN_PREFIX + jwtTokenUtils.createToken(authentication); + } + } + + public void setNamespaceHeaderAndQueryParam( + NameSpaceDetail nameSpaceDetail, HttpHeaders headers, Map<String, String> queryParams) { + headers.add("x-seata-namespace", nameSpaceDetail.getNamespace()); + if (StringUtils.isNotBlank(nameSpaceDetail.getvGroup())) { + if (queryParams != null) { + queryParams.put("vGroup", nameSpaceDetail.getvGroup()); + } + return; + } + if (nameSpaceDetail.getCluster() != null) { + headers.add("x-seata-cluster", nameSpaceDetail.getCluster()); + } + } + + @Override + public String getCallNameSpace(String path) { + HttpHeaders headers = new HttpHeaders(); + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, null, null); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody; + 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); + return "MCP GET Call NameSpace Failed: " + e.getMessage(); + } + } + + @Override + public String getCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object objectQueryParams, + Map<String, String> queryParams, + 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 { + setNamespaceHeaderAndQueryParam(nameSpaceDetail, headers, queryParams); + } + headers.add(WebSecurityConfig.AUTHORIZATION_HEADER, getToken()); + Map<String, Object> queryParamsMap = objectToQueryParamMap(objectQueryParams, objectMapper); + String url = buildUrl(String.format(NAMING_SPACE_URL, namingSpacePort), path, queryParams, queryParamsMap); + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody; + 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); + return "MCP GET Call TC Failed: " + e.getMessage(); + } Review Comment: The error messages returned to the caller expose internal system details. For example, "MCP GET Call TC Failed: " + e.getMessage() may leak sensitive information about the internal system architecture, connection details, or other implementation specifics. Consider returning generic error messages to the caller while logging detailed information for debugging purposes. ########## console/src/main/java/org/apache/seata/console/utils/JwtTokenUtils.java: ########## @@ -109,7 +109,7 @@ public Authentication getAuthentication(String token) { AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get(AUTHORITIES_KEY)); User principal = new User(claims.getSubject(), "", authorities); - return new UsernamePasswordAuthenticationToken(principal, "", authorities); + return new UsernamePasswordAuthenticationToken(principal, token, authorities); Review Comment: Storing the JWT token as credentials in the UsernamePasswordAuthenticationToken could expose the token to unauthorized access. The credentials field in authentication tokens is typically used for passwords during authentication and is often cleared after successful authentication for security reasons. By storing the JWT token here, it may be logged, cached, or accessed by code that shouldn't have access to it. Consider using a custom Authentication implementation or storing the token in a secure context if needed for later validation. ```suggestion return new UsernamePasswordAuthenticationToken(principal, null, authorities); ``` ########## console/src/main/java/org/apache/seata/mcp/entity/param/McpGlobalSessionParam.java: ########## @@ -0,0 +1,47 @@ +/* + * 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.entity.param; + +import org.apache.seata.common.util.PageUtil; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.mcp.core.utils.DateUtils; +import org.apache.seata.mcp.entity.dto.McpGlobalSessionParamDto; + +/** + * Global session param + */ +public class McpGlobalSessionParam extends org.apache.seata.server.console.entity.param.GlobalSessionParam { + + public static McpGlobalSessionParam covertFromDtoParam(McpGlobalSessionParamDto paramDto) { Review Comment: The method 'covertFromDtoParam' has a spelling error in its name. It should be 'convertFromDtoParam' (with an 'n'). This typo makes the API less professional and could cause confusion for developers using this method. ```suggestion public static McpGlobalSessionParam convertFromDtoParam(McpGlobalSessionParamParamDto paramDto) { ``` ########## namingserver/src/main/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImpl.java: ########## @@ -0,0 +1,163 @@ +/* + * 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.namingserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.metadata.ClusterRole; +import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.namingserver.NamingServerNode; +import org.apache.seata.common.util.CollectionUtils; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.ConsoleApiService; +import org.apache.seata.mcp.service.impl.ConsoleRemoteServiceImpl; +import org.apache.seata.namingserver.manager.NamingManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +import static org.apache.seata.common.Constants.RAFT_GROUP_HEADER; +import static org.apache.seata.mcp.core.utils.UrlUtils.buildUrl; +import static org.apache.seata.mcp.core.utils.UrlUtils.objectToQueryParamMap; + +@ConditionalOnBean(ConsoleRemoteServiceImpl.class) +@Primary +@Service +public class ConsoleLocalServiceImpl implements ConsoleApiService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final NamingManager namingManager; + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + public ConsoleLocalServiceImpl(NamingManager namingManager, RestTemplate restTemplate, ObjectMapper objectMapper) { + this.namingManager = namingManager; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + + public String getResult( + NameSpaceDetail nameSpaceDetail, + HttpMethod httpMethod, + String path, + Object objectQueryParams, + Map<String, String> queryParams, + HttpHeaders headers) { + String namespace = nameSpaceDetail.getNamespace(); + String cluster = nameSpaceDetail.getCluster(); + String vgroup = nameSpaceDetail.getvGroup(); + if (StringUtils.isNotBlank(namespace) && (StringUtils.isNotBlank(cluster) || StringUtils.isNotBlank(vgroup))) { + List<NamingServerNode> list = null; + if (StringUtils.isNotBlank(vgroup)) { + list = namingManager.getInstancesByVgroupAndNamespace( + namespace, vgroup, StringUtils.equalsIgnoreCase(httpMethod.name(), HttpMethod.GET.name())); + } else if (StringUtils.isNotBlank(cluster)) { + list = namingManager.getInstances(namespace, cluster); + } + if (CollectionUtils.isNotEmpty(list)) { + // Randomly select a node from the list + NamingServerNode node = list.get(ThreadLocalRandom.current().nextInt(list.size())); + Node.Endpoint controlEndpoint = node.getControl(); + if (controlEndpoint != null) { + // Construct the target URL + String baseUrl = "http://" + controlEndpoint.getHost() + ":" + controlEndpoint.getPort(); + Map<String, Object> queryParamsMap = objectToQueryParamMap(objectQueryParams, objectMapper); + String targetUrl = buildUrl(baseUrl, path, queryParams, queryParamsMap); + if (node.getRole() == ClusterRole.LEADER) { + headers.add(RAFT_GROUP_HEADER, node.getUnit()); + } + HttpEntity<String> entity = new HttpEntity<>(headers); + String responseBody = null; + try { + ResponseEntity<String> response = + restTemplate.exchange(targetUrl, httpMethod, entity, String.class); + + responseBody = response.getBody(); + + if (!response.getStatusCode().is2xxSuccessful()) { + logger.warn("MCP request returned non-success status: {}", response.getStatusCode()); + } + return responseBody; + } catch (RestClientException e) { + logger.error("MCP {} Call TC Failed: {}", httpMethod.name(), e.getMessage()); + throw new RestClientException(e.getMessage()); + } + } + } + throw new IllegalArgumentException("Couldn't find target node url"); + } + throw new IllegalArgumentException("Invalid NameSpace Detail"); + } + + @Override + public String getCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object objectQueryParams, + Map<String, String> queryParams, + HttpHeaders headers) { + return getResult(nameSpaceDetail, HttpMethod.GET, path, objectQueryParams, queryParams, headers); + } + + @Override + public String deleteCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object objectQueryParams, + Map<String, String> queryParams, + HttpHeaders headers) { + return getResult(nameSpaceDetail, HttpMethod.DELETE, path, objectQueryParams, queryParams, headers); + } + + @Override + public String putCallTC( + NameSpaceDetail nameSpaceDetail, + String path, + Object objectQueryParams, + Map<String, String> queryParams, + HttpHeaders headers) { + return getResult(nameSpaceDetail, HttpMethod.PUT, path, objectQueryParams, queryParams, headers); + } + + @Override + public String getCallNameSpace(String path) { + String namespace; + try { + namespace = objectMapper.writeValueAsString(namingManager.namespace()); + } catch (JsonProcessingException e) { + logger.error("Get NameSpace failed: {}", e.getMessage()); + throw new RuntimeException(e); Review Comment: The code throws a generic RuntimeException when JSON processing fails. This loses the original exception type and makes error handling more difficult for callers. Consider either propagating the JsonProcessingException directly (if the method signature allows) or wrapping it in a more specific custom exception type that provides better context about what operation failed. ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalSessionTools.java: ########## @@ -0,0 +1,224 @@ +/* + * 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.McpGlobalSessionParamDto; +import org.apache.seata.mcp.entity.param.McpGlobalAbnormalSessionParam; +import org.apache.seata.mcp.entity.param.McpGlobalSessionParam; +import org.apache.seata.mcp.entity.vo.McpGlobalSessionVO; +import org.apache.seata.mcp.service.ConsoleApiService; +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 ConsoleApiService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ObjectMapper objectMapper; + + private final ModifyConfirmService modifyConfirmService; + + private final List<Integer> exceptionStatus = new ArrayList<>(); + + public static int ABNORMAL_SESSION_PAGE_SIZE = 30; Review Comment: The public static variable 'ABNORMAL_SESSION_PAGE_SIZE' is not declared as final, which means it can be modified at runtime. This could lead to unexpected behavior if the value is changed elsewhere in the code. If this is intended to be a constant, declare it as 'public static final int'. If it needs to be configurable, consider making it a configuration property injected via @Value annotation instead. ########## console/src/main/java/org/apache/seata/mcp/service/impl/ConsoleRemoteServiceImpl.java: ########## @@ -0,0 +1,241 @@ +/* + * 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 com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.NamingServerLocalMarker; +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.ConsoleApiService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +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 java.util.Map; + +import static org.apache.seata.mcp.core.utils.UrlUtils.buildUrl; +import static org.apache.seata.mcp.core.utils.UrlUtils.objectToQueryParamMap; + +@ConditionalOnMissingBean(NamingServerLocalMarker.class) +@Service +public class ConsoleRemoteServiceImpl implements ConsoleApiService { + + private final JwtTokenUtils jwtTokenUtils; + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + private final AuthenticationManager authenticationManager; + + public ConsoleRemoteServiceImpl( + JwtTokenUtils jwtTokenUtils, + RestTemplate restTemplate, + ObjectMapper objectMapper, + AuthenticationManager authenticationManager) { + this.jwtTokenUtils = jwtTokenUtils; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + this.authenticationManager = authenticationManager; + } + + @Value("${seata.console.naming-space-url:http://127.0.0.1:%s}") + private String NAMING_SPACE_URL; + + private final Logger logger = LoggerFactory.getLogger(ConsoleRemoteServiceImpl.class); + + @Value("${server.port:8081}") + private String namingSpacePort; + + @Value("${seata.mcp.auth.enabled}") + private String enabledAuth; + + public String getToken() { + if (Boolean.parseBoolean(enabledAuth)) { + 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; + } else { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken("seata", ""); Review Comment: The authentication token generation for non-authenticated scenarios uses an empty password string, which could be a security concern. When 'enabledAuth' is false, the code creates authentication with username "seata" and empty password. This bypasses the normal authentication mechanism and could be exploited if the configuration is accidentally or maliciously set to false in production. Consider adding additional safeguards or logging warnings when authentication is disabled. ```suggestion logger.warn("Authentication is disabled (seata.mcp.auth.enabled=false); " + "generating token using internal fallback user. This configuration should not be used in production."); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("seata", "disabled-auth"); ``` ########## namingserver/src/main/java/org/apache/seata/namingserver/service/ConsoleLocalServiceImpl.java: ########## @@ -0,0 +1,163 @@ +/* + * 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.namingserver.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.seata.common.metadata.ClusterRole; +import org.apache.seata.common.metadata.Node; +import org.apache.seata.common.metadata.namingserver.NamingServerNode; +import org.apache.seata.common.util.CollectionUtils; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.mcp.core.props.NameSpaceDetail; +import org.apache.seata.mcp.service.ConsoleApiService; +import org.apache.seata.mcp.service.impl.ConsoleRemoteServiceImpl; +import org.apache.seata.namingserver.manager.NamingManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; + +import static org.apache.seata.common.Constants.RAFT_GROUP_HEADER; +import static org.apache.seata.mcp.core.utils.UrlUtils.buildUrl; +import static org.apache.seata.mcp.core.utils.UrlUtils.objectToQueryParamMap; + +@ConditionalOnBean(ConsoleRemoteServiceImpl.class) +@Primary +@Service +public class ConsoleLocalServiceImpl implements ConsoleApiService { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final NamingManager namingManager; + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper; + + public ConsoleLocalServiceImpl(NamingManager namingManager, RestTemplate restTemplate, ObjectMapper objectMapper) { + this.namingManager = namingManager; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + + public String getResult( + NameSpaceDetail nameSpaceDetail, + HttpMethod httpMethod, + String path, + Object objectQueryParams, + Map<String, String> queryParams, + HttpHeaders headers) { + String namespace = nameSpaceDetail.getNamespace(); + String cluster = nameSpaceDetail.getCluster(); + String vgroup = nameSpaceDetail.getvGroup(); + if (StringUtils.isNotBlank(namespace) && (StringUtils.isNotBlank(cluster) || StringUtils.isNotBlank(vgroup))) { + List<NamingServerNode> list = null; + if (StringUtils.isNotBlank(vgroup)) { + list = namingManager.getInstancesByVgroupAndNamespace( + namespace, vgroup, StringUtils.equalsIgnoreCase(httpMethod.name(), HttpMethod.GET.name())); Review Comment: The HTTP method comparison uses string comparison with case-insensitive check, but this is unnecessarily complex. The HttpMethod enum provides an equals() method that can be used directly for comparison. Consider changing `StringUtils.equalsIgnoreCase(httpMethod.name(), HttpMethod.GET.name())` to `HttpMethod.GET.equals(httpMethod)` for better readability and performance. ########## console/src/main/java/org/apache/seata/mcp/service/impl/ModifyConfirmServiceImpl.java: ########## @@ -0,0 +1,73 @@ +/* + * 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 io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import javax.crypto.spec.SecretKeySpec; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class ModifyConfirmServiceImpl implements ModifyConfirmService { + + private final JwtTokenUtils jwtTokenUtils; + + private static final long MODIFY_TOKEN_VALIDITY_IN_MILLISECONDS = 60_000; Review Comment: The modify key has a very short validity period of only 60 seconds (MODIFY_TOKEN_VALIDITY_IN_MILLISECONDS = 60_000). This may not be sufficient time for users to complete their confirmation and modification action, especially if there are network delays or if the user needs to review the change carefully. Consider increasing this timeout to a more reasonable value like 5 minutes (300_000 ms) to provide better user experience while still maintaining security. ```suggestion private static final long MODIFY_TOKEN_VALIDITY_IN_MILLISECONDS = 300_000; ``` ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalSessionTools.java: ########## @@ -0,0 +1,224 @@ +/* + * 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.McpGlobalSessionParamDto; +import org.apache.seata.mcp.entity.param.McpGlobalAbnormalSessionParam; +import org.apache.seata.mcp.entity.param.McpGlobalSessionParam; +import org.apache.seata.mcp.entity.vo.McpGlobalSessionVO; +import org.apache.seata.mcp.service.ConsoleApiService; +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 ConsoleApiService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ObjectMapper objectMapper; + + private final ModifyConfirmService modifyConfirmService; + + private final List<Integer> exceptionStatus = new ArrayList<>(); + + public static int ABNORMAL_SESSION_PAGE_SIZE = 30; + + public GlobalSessionTools( + ConsoleApiService mcpRPCService, + MCPProperties mcpProperties, + ObjectMapper objectMapper, + ModifyConfirmService modifyConfirmService) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.objectMapper = objectMapper; + this.modifyConfirmService = modifyConfirmService; + exceptionStatus.add(GlobalStatus.CommitFailed.getCode()); + exceptionStatus.add(GlobalStatus.TimeoutRollbackFailed.getCode()); + exceptionStatus.add(GlobalStatus.RollbackFailed.getCode()); + } + + @McpTool(description = "Query global transactions") + public PageResult<McpGlobalSessionVO> queryGlobalSession( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Query parameter objects") McpGlobalSessionParamDto paramDto) { + McpGlobalSessionParam param = McpGlobalSessionParam.covertFromDtoParam(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + return PageResult.failure( + "", + String.format( + "The query time span is not allowed to exceed the max query duration: %s hour", + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()))); + } + } else { + param.setTimeEnd(param.getTimeStart() + DateUtils.ONE_DAY_TIMESTAMP); + } + } else { + param.setTimeEnd(null); + param.setTimeStart(null); + } + PageResult<McpGlobalSessionVO> pageResult; + String result = mcpRPCService.getCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_SESSION_BASE_URL + "/query", param, null, null); + try { + pageResult = objectMapper.readValue(result, new TypeReference<PageResult<McpGlobalSessionVO>>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); Review Comment: The method throws a generic RuntimeException when JSON processing fails. This loses the specific exception type (JsonProcessingException) and makes error handling more difficult. Consider either propagating the original exception type or wrapping it in a more specific custom exception that provides better context about what operation failed. ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalLockTools.java: ########## @@ -0,0 +1,133 @@ +/* + * 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.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.McpGlobalLockParamDto; +import org.apache.seata.mcp.entity.param.McpGlobalLockDeleteParam; +import org.apache.seata.mcp.entity.param.McpGlobalLockParam; +import org.apache.seata.mcp.entity.vo.McpGlobalLockVO; +import org.apache.seata.mcp.service.ConsoleApiService; +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.HashMap; +import java.util.Map; + +@Service +public class GlobalLockTools { + + private final ConsoleApiService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ModifyConfirmService modifyConfirmService; + + private final ObjectMapper objectMapper; + + public GlobalLockTools( + ConsoleApiService mcpRPCService, + MCPProperties mcpProperties, + ModifyConfirmService modifyConfirmService, + ObjectMapper objectMapper) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.modifyConfirmService = modifyConfirmService; + this.objectMapper = objectMapper; + } + + @McpTool(description = "Query the global lock information") + public PageResult<McpGlobalLockVO> queryGlobalLock( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global lock parameters") McpGlobalLockParamDto paramDto) { + McpGlobalLockParam param = McpGlobalLockParam.convertFromParamDto(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + return PageResult.failure( + "", + String.format( + "The query time span is not allowed to exceed the max query duration: %s hour", + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()))); Review Comment: The error message format is inconsistent with other similar error messages in GlobalSessionTools. This message says "The query time span is not allowed to exceed the max query duration: %s hour" while GlobalSessionTools uses a similar but slightly different format. Consider standardizing the error message format across both classes for consistency. ########## console/src/main/java/org/apache/seata/mcp/tools/GlobalLockTools.java: ########## @@ -0,0 +1,133 @@ +/* + * 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.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.McpGlobalLockParamDto; +import org.apache.seata.mcp.entity.param.McpGlobalLockDeleteParam; +import org.apache.seata.mcp.entity.param.McpGlobalLockParam; +import org.apache.seata.mcp.entity.vo.McpGlobalLockVO; +import org.apache.seata.mcp.service.ConsoleApiService; +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.HashMap; +import java.util.Map; + +@Service +public class GlobalLockTools { + + private final ConsoleApiService mcpRPCService; + + private final MCPProperties mcpProperties; + + private final ModifyConfirmService modifyConfirmService; + + private final ObjectMapper objectMapper; + + public GlobalLockTools( + ConsoleApiService mcpRPCService, + MCPProperties mcpProperties, + ModifyConfirmService modifyConfirmService, + ObjectMapper objectMapper) { + this.mcpRPCService = mcpRPCService; + this.mcpProperties = mcpProperties; + this.modifyConfirmService = modifyConfirmService; + this.objectMapper = objectMapper; + } + + @McpTool(description = "Query the global lock information") + public PageResult<McpGlobalLockVO> queryGlobalLock( + @McpToolParam(description = "Specify the namespace of the TC node") NameSpaceDetail nameSpaceDetail, + @McpToolParam(description = "Global lock parameters") McpGlobalLockParamDto paramDto) { + McpGlobalLockParam param = McpGlobalLockParam.convertFromParamDto(paramDto); + if (param.getTimeStart() != null) { + if (param.getTimeEnd() != null) { + if (DateUtils.judgeExceedTimeDuration( + param.getTimeStart(), param.getTimeEnd(), mcpProperties.getQueryDuration())) { + return PageResult.failure( + "", + String.format( + "The query time span is not allowed to exceed the max query duration: %s hour", + DateUtils.convertToHourFromTimeStamp(mcpProperties.getQueryDuration()))); + } + } else { + param.setTimeEnd(param.getTimeStart() + DateUtils.ONE_DAY_TIMESTAMP); + } + } else { + param.setTimeEnd(null); + param.setTimeStart(null); + } + PageResult<McpGlobalLockVO> result; + String response = mcpRPCService.getCallTC( + nameSpaceDetail, RPCConstant.GLOBAL_LOCK_BASE_URL + "/query", param, null, null); + try { + result = objectMapper.readValue(response, new TypeReference<PageResult<McpGlobalLockVO>>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); Review Comment: The method throws a generic RuntimeException when JSON processing fails. This loses the specific exception type (JsonProcessingException) and makes error handling more difficult. Consider either propagating the original exception type or wrapping it in a more specific custom exception that provides better context about what operation failed. ########## console/src/main/java/org/apache/seata/mcp/service/impl/ModifyConfirmServiceImpl.java: ########## @@ -0,0 +1,73 @@ +/* + * 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 io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import org.apache.seata.console.utils.JwtTokenUtils; +import org.apache.seata.mcp.service.ModifyConfirmService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import javax.crypto.spec.SecretKeySpec; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class ModifyConfirmServiceImpl implements ModifyConfirmService { + + private final JwtTokenUtils jwtTokenUtils; + + private static final long MODIFY_TOKEN_VALIDITY_IN_MILLISECONDS = 60_000; + + @Value("${seata.security.secretKey}") + private String secretKey; + + public ModifyConfirmServiceImpl(JwtTokenUtils jwtTokenUtils) { + this.jwtTokenUtils = jwtTokenUtils; + } + + @Override + public Map<String, String> confirmAndGetKey() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + long now = (new Date()).getTime(); + Date expirationDate = new Date(now + MODIFY_TOKEN_VALIDITY_IN_MILLISECONDS); + SecretKeySpec secretKeySpec = + new SecretKeySpec(Decoders.BASE64.decode(secretKey), SignatureAlgorithm.HS256.getJcaName()); + String key = Jwts.builder() + .setSubject(authentication.getName()) + .claim("modify", "") + .setExpiration(expirationDate) + .signWith(secretKeySpec, SignatureAlgorithm.HS256) + .compact(); + Map<String, String> map = new HashMap<>(); + map.put("modify_key", key); + map.put( + "Important!!!", + "You need to repeat the content to be modified by the user and get confirmation from the user before you can continue to call the modification tool"); Review Comment: The hardcoded message in the map with key "Important!!!" is inappropriate for a production API response. This type of exclamatory message should be part of documentation or user interface guidance, not embedded in the API response data. Consider removing this entry or replacing it with a structured field like "expiresInSeconds" or "usage" with a professional description. ```suggestion "expiresInSeconds", String.valueOf(MODIFY_TOKEN_VALIDITY_IN_MILLISECONDS / 1000)); ``` ########## console/src/main/java/org/apache/seata/mcp/entity/vo/McpGlobalSessionVO.java: ########## @@ -0,0 +1,75 @@ +/* + * 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.entity.vo; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.apache.seata.mcp.core.config.TimestampToStringDeserializer; + +import java.util.Set; + +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; +import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; + +@JsonAutoDetect(getterVisibility = NONE, fieldVisibility = ANY) +public class McpGlobalSessionVO extends org.apache.seata.server.console.entity.vo.GlobalSessionVO { + + private String beginTime; + private String gmtCreate; + private String gmtModified; + private Set<McpBranchSessionVO> branchSessionVOs; + + @JsonProperty("beginTime") + @JsonDeserialize(using = TimestampToStringDeserializer.class) + public String getBegin() { + return beginTime; + } + + public void setBeginTime(String beginTime) { + this.beginTime = beginTime; + } + + @JsonProperty("gmtCreate") + @JsonDeserialize(using = TimestampToStringDeserializer.class) + public String getCreate() { + return gmtCreate; + } + + public void setGmtCreate(String gmtCreate) { + this.gmtCreate = gmtCreate; + } + + @JsonProperty("gmtModified") + @JsonDeserialize(using = TimestampToStringDeserializer.class) + public String getModified() { + return gmtModified; + } + + public void setGmtModified(String gmtModified) { + this.gmtModified = gmtModified; + } + + @JsonProperty("branchSessionVOs") + public Set<McpBranchSessionVO> getBranchSessionVO() { + return branchSessionVOs; + } + + public void setBranchSessionVO(Set<McpBranchSessionVO> mcpBranchSessionVOS) { + this.branchSessionVOs = mcpBranchSessionVOS; Review Comment: The method setBranchSessionVO uses inconsistent naming with its corresponding getter getBranchSessionVO. The parameter name is 'mcpBranchSessionVOS' (plural with 'S' suffix) while the field is 'branchSessionVOs' (plural with 's' suffix and different case). The parameter should be renamed to 'branchSessionVOs' to match the field name and maintain consistency. ```suggestion public void setBranchSessionVO(Set<McpBranchSessionVO> branchSessionVOs) { this.branchSessionVOs = branchSessionVOs; ``` -- 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]
