merge: remote updates into main with tool-core schema updates
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
This commit is contained in:
@@ -16,9 +16,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if command -v apt-get &> /dev/null; then
|
if command -v apt-get &> /dev/null; then
|
||||||
apt-get update
|
apt-get update
|
||||||
apt-get install -y nodejs git openjdk-21-jdk
|
apt-get install -y nodejs git openjdk-21-jdk rsync
|
||||||
elif command -v apk &> /dev/null; then
|
elif command -v apk &> /dev/null; then
|
||||||
apk add --no-cache nodejs git openjdk21
|
apk add --no-cache nodejs git openjdk21 rsync
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
@@ -26,8 +26,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Sync Code to Host Volume
|
- name: Sync Code to Host Volume
|
||||||
run: |
|
run: |
|
||||||
echo "Copying latest code to /app (Host Volume)..."
|
echo "Copying latest code to /app (Host Volume) and removing stale files..."
|
||||||
cp -a . /app/
|
if command -v rsync &> /dev/null; then
|
||||||
|
rsync -a --delete --exclude='.git' . /app/
|
||||||
|
else
|
||||||
|
rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle
|
||||||
|
cp -a . /app/
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Deploy Task on Host
|
- name: Deploy Task on Host
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# DAP Backend
|
# DAP Backend
|
||||||
|
|
||||||
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다.
|
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트 입니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ public class ExecuteService {
|
|||||||
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
|
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
|
||||||
targetUrl = metadata.getPodUrl();
|
targetUrl = metadata.getPodUrl();
|
||||||
}
|
}
|
||||||
String executeApiUrl = targetUrl + "/mcp/" + metadata.getName();
|
|
||||||
|
|
||||||
Map<String, String> headers = new java.util.HashMap<>();
|
Map<String, String> headers = new java.util.HashMap<>();
|
||||||
headers.put("trace-id", context.requestId());
|
headers.put("trace-id", context.requestId());
|
||||||
@@ -279,16 +278,16 @@ public class ExecuteService {
|
|||||||
|
|
||||||
JsonNode data = null;
|
JsonNode data = null;
|
||||||
try {
|
try {
|
||||||
data = toolInvoker.invoke(pagePayload, executeApiUrl, headers);
|
data = toolInvoker.invoke(metadata.getName(), pagePayload, targetUrl, headers);
|
||||||
} catch (org.springframework.web.client.RestClientResponseException e) {
|
} catch (org.springframework.web.client.RestClientResponseException e) {
|
||||||
// HTTP 4xx, 5xx 에러는 연결 오류가 아니라 비즈니스 로직 오류이거나 검증 실패이므로 원본 에러를 그대로 반환
|
// HTTP 4xx, 5xx 에러는 연결 오류가 아니라 비즈니스 로직 오류이거나 검증 실패이므로 원본 에러를 그대로 반환
|
||||||
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 에러 (" + e.getStatusCode() + "): " + e.getResponseBodyAsString());
|
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 에러 (" + e.getStatusCode() + "): " + e.getResponseBodyAsString());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (executeApiUrl.contains("http://tool-")) {
|
if (targetUrl.contains("http://tool-")) {
|
||||||
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
|
String fallbackUrl = targetUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
|
||||||
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
|
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
|
||||||
try {
|
try {
|
||||||
data = toolInvoker.invoke(pagePayload, fallbackUrl, headers);
|
data = toolInvoker.invoke(metadata.getName(), pagePayload, fallbackUrl, headers);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,81 @@
|
|||||||
package io.shinhanlife.dap.mcg.transport;
|
package io.shinhanlife.dap.mcg.transport;
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package io.shinhanlife.dap.mcg.transport
|
|
||||||
* @className HttpToolInvoker
|
|
||||||
* @description AX HUB 시스템 처리 클래스
|
|
||||||
* @author 0986406
|
|
||||||
* @create 2026.09.01
|
|
||||||
* <pre>
|
|
||||||
* ---------- 개정이력 ----------
|
|
||||||
* 수정일 수정자 수정내용
|
|
||||||
* ---------- -------- ---------------------------
|
|
||||||
* 2026.09.01 0986406 최초생성
|
|
||||||
*
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
|
||||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.client.RestClient;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import com.fasterxml.jackson.databind.node.TextNode;
|
||||||
|
import io.modelcontextprotocol.client.McpClient;
|
||||||
|
import io.modelcontextprotocol.client.McpSyncClient;
|
||||||
|
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
||||||
|
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** MCP SDK client used for Gateway-to-Tool-Pod calls over Streamable HTTP. */
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class HttpToolInvoker implements ToolInvoker {
|
public class HttpToolInvoker implements ToolInvoker {
|
||||||
|
|
||||||
private final RestClient restClient;
|
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public HttpToolInvoker(ObjectMapper objectMapper) {
|
public HttpToolInvoker(ObjectMapper objectMapper) {
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.restClient = RestClient.create();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JsonNode invoke(Map<String, Object> payload, String targetUrl, Map<String, String> headers) {
|
public JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers) {
|
||||||
try {
|
String endpoint = podUrl.replaceAll("/+$", "") + "/mcp";
|
||||||
RestClient.RequestBodySpec requestSpec = restClient.post()
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
|
||||||
.uri(targetUrl)
|
if (headers != null) {
|
||||||
.contentType(MediaType.APPLICATION_JSON);
|
headers.forEach(requestBuilder::header);
|
||||||
|
}
|
||||||
|
|
||||||
if (headers != null) {
|
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(endpoint)
|
||||||
headers.forEach(requestSpec::header);
|
.requestBuilder(requestBuilder)
|
||||||
|
.connectTimeout(Duration.ofSeconds(5))
|
||||||
|
.build();
|
||||||
|
try (McpSyncClient client = McpClient.sync(transport)
|
||||||
|
.clientInfo(new McpSchema.Implementation("dap-gateway", "1.0.0"))
|
||||||
|
.requestTimeout(Duration.ofSeconds(30))
|
||||||
|
.build()) {
|
||||||
|
client.initialize();
|
||||||
|
McpSchema.CallToolResult result = client.callTool(McpSchema.CallToolRequest.builder()
|
||||||
|
.name(toolName)
|
||||||
|
.arguments(arguments)
|
||||||
|
.build());
|
||||||
|
if (Boolean.TRUE.equals(result.isError())) {
|
||||||
|
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool Pod MCP error: " + textContent(result));
|
||||||
}
|
}
|
||||||
|
return result.structuredContent() != null
|
||||||
Object httpResult = requestSpec.body(payload)
|
? objectMapper.valueToTree(result.structuredContent())
|
||||||
.retrieve()
|
: textContentAsJson(result);
|
||||||
.body(Object.class);
|
} catch (ToolExecutionException error) {
|
||||||
|
throw error;
|
||||||
return extractData(objectMapper.valueToTree(httpResult));
|
} catch (Exception error) {
|
||||||
} catch (Exception e) {
|
throw new ToolExecutionException(FailureType.SERVER_ERROR,
|
||||||
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 호출 실패: " + e.getMessage(), e);
|
"Tool Pod MCP call failed: " + error.getMessage(), error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode extractData(JsonNode root) {
|
private JsonNode textContentAsJson(McpSchema.CallToolResult result) {
|
||||||
if (!root.path("success").asBoolean(true)) {
|
String text = textContent(result);
|
||||||
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
|
try {
|
||||||
|
return objectMapper.readTree(text);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return TextNode.valueOf(text);
|
||||||
}
|
}
|
||||||
return root.has("data") ? root.get("data") : root;
|
}
|
||||||
|
|
||||||
|
private String textContent(McpSchema.CallToolResult result) {
|
||||||
|
return result.content().stream()
|
||||||
|
.filter(McpSchema.TextContent.class::isInstance)
|
||||||
|
.map(McpSchema.TextContent.class::cast)
|
||||||
|
.map(McpSchema.TextContent::text)
|
||||||
|
.findFirst()
|
||||||
|
.orElse("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,5 +22,5 @@ import java.util.Map;
|
|||||||
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
|
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
|
||||||
*/
|
*/
|
||||||
public interface ToolInvoker {
|
public interface ToolInvoker {
|
||||||
JsonNode invoke(Map<String, Object> payload, String targetUrl, Map<String, String> headers);
|
JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ dependencies {
|
|||||||
|
|
||||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
||||||
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
api 'com.networknt:json-schema-validator:1.4.0'
|
api 'com.networknt:json-schema-validator:3.0.0'
|
||||||
api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
||||||
api 'org.springframework.kafka:spring-kafka:3.2.0'
|
api 'org.springframework.kafka:spring-kafka:3.2.0'
|
||||||
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
|
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ package io.shinhanlife.dap.mcc.presentation;
|
|||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.JsonSchema;
|
import com.networknt.schema.Error;
|
||||||
import com.networknt.schema.JsonSchemaFactory;
|
|
||||||
import com.networknt.schema.SpecVersion;
|
|
||||||
import com.networknt.schema.ValidationMessage;
|
|
||||||
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpTool;
|
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||||
@@ -58,6 +55,7 @@ public class BusinessToolController {
|
|||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final McpProperties mcpProperties;
|
private final McpProperties mcpProperties;
|
||||||
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
||||||
|
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
||||||
|
|
||||||
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
||||||
@GetMapping("/mcp/api/v1/tools/local")
|
@GetMapping("/mcp/api/v1/tools/local")
|
||||||
@@ -149,17 +147,12 @@ public class BusinessToolController {
|
|||||||
if (!Map.class.isAssignableFrom(paramType)) {
|
if (!Map.class.isAssignableFrom(paramType)) {
|
||||||
try {
|
try {
|
||||||
Map<String, Object> autoSchema = JsonSchemaGenerator.generateSchema(paramType);
|
Map<String, Object> autoSchema = JsonSchemaGenerator.generateSchema(paramType);
|
||||||
String fullSchemaJson = objectMapper.writeValueAsString(autoSchema);
|
List<Error> errors = toolArgumentSchemaValidator.validate(autoSchema, arguments);
|
||||||
|
|
||||||
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
|
||||||
JsonSchema schema = factory.getSchema(fullSchemaJson);
|
|
||||||
|
|
||||||
Set<ValidationMessage> errors = schema.validate(objectMapper.valueToTree(arguments));
|
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
||||||
List<String> errorMessages = new ArrayList<>();
|
List<String> errorMessages = new ArrayList<>();
|
||||||
for (ValidationMessage vm : errors) {
|
for (Error validationError : errors) {
|
||||||
errorMessages.add(vm.getMessage());
|
errorMessages.add(validationError.getMessage());
|
||||||
}
|
}
|
||||||
Map<String, Object> errorDetails = new HashMap<>();
|
Map<String, Object> errorDetails = new HashMap<>();
|
||||||
errorDetails.put("status", "422");
|
errorDetails.put("status", "422");
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package io.shinhanlife.dap.mcc.presentation;
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.JsonSchema;
|
import com.networknt.schema.Error;
|
||||||
import com.networknt.schema.JsonSchemaFactory;
|
import com.networknt.schema.InputFormat;
|
||||||
import com.networknt.schema.SpecVersion;
|
import com.networknt.schema.Schema;
|
||||||
import com.networknt.schema.ValidationMessage;
|
import com.networknt.schema.SchemaRegistry;
|
||||||
import java.util.Set;
|
import com.networknt.schema.SpecificationVersion;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@@ -20,11 +20,9 @@ public class ToolArgumentSchemaValidator {
|
|||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<ValidationMessage> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
|
public List<Error> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
|
||||||
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
|
||||||
JsonNode schemaNode = objectMapper.valueToTree(schemaDefinition);
|
Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition));
|
||||||
JsonSchema schema = factory.getSchema(schemaNode);
|
return schema.validate(objectMapper.writeValueAsString(arguments), InputFormat.JSON);
|
||||||
JsonNode argumentsNode = objectMapper.valueToTree(arguments);
|
|
||||||
return schema.validate(argumentsNode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.networknt.schema.JsonSchema;
|
import com.networknt.schema.Error;
|
||||||
import com.networknt.schema.JsonSchemaFactory;
|
import com.networknt.schema.InputFormat;
|
||||||
import com.networknt.schema.SpecVersion;
|
import com.networknt.schema.Schema;
|
||||||
import com.networknt.schema.ValidationMessage;
|
import com.networknt.schema.SchemaRegistry;
|
||||||
|
import com.networknt.schema.SpecificationVersion;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||||
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
import io.shinhanlife.dap.lib.annotation.McpValidation;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,10 +65,10 @@ class JsonSchemaGeneratorTest {
|
|||||||
@Test
|
@Test
|
||||||
void validatorRejectsInvalidNestedValue() throws Exception {
|
void validatorRejectsInvalidNestedValue() throws Exception {
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
JsonSchema schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)
|
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7)
|
||||||
.getSchema(objectMapper.writeValueAsString(JsonSchemaGenerator.generateSchema(NestedRequest.class)));
|
.getSchema(objectMapper.writeValueAsString(JsonSchemaGenerator.generateSchema(NestedRequest.class)));
|
||||||
Set<ValidationMessage> errors = schema.validate(objectMapper.valueToTree(Map.of(
|
List<Error> errors = schema.validate(objectMapper.writeValueAsString(Map.of(
|
||||||
"child", Map.of("businessDate", "2026-07-28"))));
|
"child", Map.of("businessDate", "2026-07-28"))), InputFormat.JSON);
|
||||||
|
|
||||||
assertFalse(errors.isEmpty());
|
assertFalse(errors.isEmpty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import lombok.Data;
|
|||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class BalanceRequest {
|
public class BalanceRequest {
|
||||||
@McpParameter(description = "고객의 계좌번호 (- 제외)", required = true)
|
@McpParameter(description = "고객의 계좌번호 (- 제외) ", required = true)
|
||||||
@McpValidation(pattern = "\\S")
|
@McpValidation(pattern = "\\S")
|
||||||
private String accountNumber;
|
private String accountNumber;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,3 +37,7 @@ axhub:
|
|||||||
url: http://localhost:8081
|
url: http://localhost:8081
|
||||||
tool:
|
tool:
|
||||||
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
|
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
|
||||||
|
|
||||||
|
sol:
|
||||||
|
req-detail:
|
||||||
|
mock-enabled: true
|
||||||
|
|||||||
Reference in New Issue
Block a user