Merge remote-tracking branch 'origin/main'
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package io.shinhanlife.dap.lib.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks a response DTO whose generated JSON Schema must be exposed and validated for a Tool response.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface McpOutputSchema {
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.ai.mcp.annotation.McpTool;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ToolSlaMonitoringAspect {
|
||||
private final McpProperties mcpProperties;
|
||||
|
||||
// @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
|
||||
@Around("@annotation(org.springframework.ai.mcp.annotation.McpTool)")
|
||||
@Around("@annotation(org.springaicommunity.mcp.annotation.McpTool)")
|
||||
public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
/**
|
||||
* Registers the temporary Glow HTTP compatibility component from the DAP library scan scope.
|
||||
* The bean is only created when an official GlowHttpComponent has not already been supplied.
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class AxhubHttpConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(GlowHttpComponent.class)
|
||||
public GlowHttpComponent glowHttpComponent(RestClient.Builder restClientBuilder) {
|
||||
return new GlowHttpComponent(restClientBuilder);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package io.shinhanlife.dap.lib.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -15,4 +16,9 @@ public class ToolSchemaConfiguration {
|
||||
public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) {
|
||||
return new ToolSchemaResolver(objectMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper) {
|
||||
return new ToolArgumentSchemaValidator(objectMapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package io.shinhanlife.dap.lib.integration.http.component;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
||||
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
|
||||
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
|
||||
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
|
||||
import io.shinhanlife.glow.communication.module.http.dto.HttpBody;
|
||||
import io.shinhanlife.glow.communication.module.http.dto.HttpHeader;
|
||||
import io.shinhanlife.glow.communication.module.http.dto.HttpTransfer;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Tool Pod outbound HTTP component modelled after the ShinhanLife HTTP component.
|
||||
* It resolves an API domain from configuration and delegates the assembled HttpTransfer to Glow.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AxhubHttpComponent {
|
||||
|
||||
private static final String ANONYMOUS_REQUEST = "AXHUB-TOOL";
|
||||
|
||||
private final GlowHttpComponent http;
|
||||
private final ObjectMapper json;
|
||||
private final GlowCommunicationProperties communicationProperties;
|
||||
private final AxhubHttpProperties properties;
|
||||
|
||||
public <T, R> R call(AxhubHttpDomain domain, String uri, T inputDto, Class<R> responseBodyClass) {
|
||||
return call(domain, uri, inputDto, responseBodyClass, 0);
|
||||
}
|
||||
|
||||
public <T, R> R call(AxhubHttpDomain domain, String uri, T inputDto, Class<R> responseBodyClass, int timeout) {
|
||||
if (uri == null || uri.isBlank()) {
|
||||
throw new IllegalArgumentException("URI is required.");
|
||||
}
|
||||
AxhubHttpProperties.ApiDefinition api = resolveApi(domain);
|
||||
HttpHeader header = createHeader(api, timeout);
|
||||
String requestUri = joinPath(api.path(), uri);
|
||||
HttpTransfer<T> request = HttpTransfer.<T>http()
|
||||
.header(header)
|
||||
.domain(api.domain())
|
||||
.uri(requestUri)
|
||||
.method(api.method())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.responseEntity(responseBodyClass)
|
||||
.body(inputDto)
|
||||
.build();
|
||||
|
||||
log.info("[AxhubHttpComponent] Glow HTTP call. domain={}, method={}, uri={}",
|
||||
domain.getCode(), api.method(), requestUri);
|
||||
ResponseEntity<HttpBody> response = http.sync(request);
|
||||
return convertResponse(response.getBody(), responseBodyClass);
|
||||
}
|
||||
|
||||
/** Convenience method for APIs configured as internal business Pods. */
|
||||
public <T, R> R callBizPod(AxhubHttpDomain domain, String uri, T inputDto, Class<R> responseBodyClass) {
|
||||
AxhubHttpProperties.ApiDefinition api = resolveApi(domain);
|
||||
if (!api.bizPod()) {
|
||||
throw new IllegalArgumentException("Configured API is not a business Pod: " + domain.getCode());
|
||||
}
|
||||
return call(domain, uri, inputDto, responseBodyClass);
|
||||
}
|
||||
|
||||
private AxhubHttpProperties.ApiDefinition resolveApi(AxhubHttpDomain domain) {
|
||||
return properties.getApiList().stream()
|
||||
.filter(api -> domain.getCode().equals(api.name()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No HTTP API configuration for domain: " + domain.getCode()));
|
||||
}
|
||||
|
||||
private HttpHeader createHeader(AxhubHttpProperties.ApiDefinition api, int timeout) {
|
||||
HttpHeader header = new HttpHeader();
|
||||
if (api.bizPod()) {
|
||||
header.set("X-POD-TO-POD", "true");
|
||||
}
|
||||
McpRequestHeaders inbound = McpRequestHeaderContext.current();
|
||||
if (inbound == null) {
|
||||
header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST);
|
||||
} else {
|
||||
putIfPresent(header, "trace-id", inbound.traceId());
|
||||
putIfPresent(header, "request-id", inbound.requestId());
|
||||
putIfPresent(header, "X-USER-ID", inbound.encryptedEmployeeId());
|
||||
}
|
||||
header.setReadTimeout(timeout == 0 ? defaultReadTimeout() : timeout);
|
||||
return header;
|
||||
}
|
||||
|
||||
private int defaultReadTimeout() {
|
||||
return communicationProperties == null || communicationProperties.getHttp() == null
|
||||
? 0 : communicationProperties.getHttp().getReadTimeout();
|
||||
}
|
||||
|
||||
private <R> R convertResponse(HttpBody responseBody, Class<R> responseBodyClass) {
|
||||
String content = responseBody == null ? null : responseBody.content();
|
||||
if (responseBodyClass == String.class) {
|
||||
return responseBodyClass.cast(content);
|
||||
}
|
||||
try {
|
||||
return json.readValue(content, responseBodyClass);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to convert HTTP response to " + responseBodyClass.getSimpleName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void putIfPresent(HttpHeader header, String name, String value) {
|
||||
if (value != null && !value.isBlank()) {
|
||||
header.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private String joinPath(String basePath, String uri) {
|
||||
String left = basePath == null ? "" : basePath.replaceAll("/+$", "");
|
||||
String right = uri.startsWith("/") ? uri : "/" + uri;
|
||||
return left + right;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.shinhanlife.dap.lib.integration.http.component;
|
||||
|
||||
/** Registered outbound HTTP API domains. Add a domain only after its endpoint is configured. */
|
||||
public enum AxhubHttpDomain {
|
||||
SAMPLE("sample");
|
||||
|
||||
private final String code;
|
||||
|
||||
AxhubHttpDomain(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.shinhanlife.dap.lib.integration.http.component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Domain-to-endpoint configuration for outbound Tool HTTP calls. */
|
||||
@Getter
|
||||
@Setter
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "glow.communication.http")
|
||||
public class AxhubHttpProperties {
|
||||
|
||||
private List<ApiDefinition> apiList = new ArrayList<>();
|
||||
|
||||
public record ApiDefinition(String name, String domain, String path, HttpMethod method, boolean bizPod) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.networknt.schema.Error;
|
||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Executes a cached Tool independently from its HTTP or MCP transport. */
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class McpToolExecutionService {
|
||||
|
||||
private final McpToolMethodRegistry toolMethodRegistry;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
||||
private final ToolSchemaResolver toolSchemaResolver;
|
||||
|
||||
public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders,
|
||||
Map<String, Object> arguments) {
|
||||
String headerRequestId = requestHeaders == null ? null : requestHeaders.headerRequestId();
|
||||
String traceId = requestHeaders == null ? null : requestHeaders.traceId();
|
||||
String requestId = requestHeaders == null ? null : requestHeaders.requestId();
|
||||
log.info("[Tool] IN - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName);
|
||||
|
||||
McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName);
|
||||
if (resolvedTool == null) {
|
||||
return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, headerRequestId);
|
||||
}
|
||||
ToolExecutionResult validationFailure = validateInput(resolvedTool, arguments, headerRequestId);
|
||||
if (validationFailure != null) {
|
||||
return validationFailure;
|
||||
}
|
||||
try {
|
||||
Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments));
|
||||
ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, headerRequestId);
|
||||
if (outputFailure != null) {
|
||||
return outputFailure;
|
||||
}
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
if (traceId != null) headers.put("trace-id", traceId);
|
||||
if (requestId != null) headers.put("request-id", requestId);
|
||||
log.info("[Tool] OUT - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName);
|
||||
return new ToolExecutionResult(200, methodResult, headers);
|
||||
} catch (Exception error) {
|
||||
log.error("[Tool] Tool execution failed. tool={}", functionName, error);
|
||||
return error(502, "TOOL_ERROR", "Tool execution failed", headerRequestId);
|
||||
}
|
||||
}
|
||||
|
||||
private ToolExecutionResult validateInput(McpToolMethodRegistry.RegisteredTool tool,
|
||||
Map<String, Object> arguments, String requestId) {
|
||||
if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null;
|
||||
try {
|
||||
Map<String, Object> schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]);
|
||||
List<Error> errors = toolArgumentSchemaValidator.validate(schema, arguments);
|
||||
return errors.isEmpty() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId);
|
||||
} catch (Exception error) {
|
||||
log.error("[Tool] Input schema validation failed unexpectedly. tool={}", tool.annotation().name(), error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Object convertArgument(Method method, Map<String, Object> arguments) {
|
||||
if (method.getParameterCount() == 0 || arguments == null || Map.class.isAssignableFrom(method.getParameterTypes()[0])) return arguments;
|
||||
return objectMapper.convertValue(arguments, method.getParameterTypes()[0]);
|
||||
}
|
||||
|
||||
private Object invoke(McpToolMethodRegistry.RegisteredTool tool, Object argument) throws Exception {
|
||||
return tool.method().getParameterCount() == 0 ? tool.method().invoke(tool.bean()) : tool.method().invoke(tool.bean(), argument);
|
||||
}
|
||||
|
||||
private ToolExecutionResult validateOutput(McpToolMethodRegistry.RegisteredTool tool,
|
||||
Object methodResult, String requestId) {
|
||||
try {
|
||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint());
|
||||
if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).isEmpty()) {
|
||||
return error(500, "INVALID_TOOL_RESPONSE", "Tool response does not match its output schema", requestId);
|
||||
}
|
||||
} catch (Exception error) {
|
||||
log.error("[Tool] Output schema validation failed unexpectedly. tool={}", tool.annotation().name(), error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ToolExecutionResult error(int statusCode, String code, String message, String requestId) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("code", code);
|
||||
body.put("message", message);
|
||||
body.put("details", Map.of("status", Integer.toString(statusCode)));
|
||||
if (requestId != null) body.put("request_id", requestId);
|
||||
return new ToolExecutionResult(statusCode, body, Map.of());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Caches executable {@link McpTool} methods once when a Tool Pod starts.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class McpToolMethodRegistry {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final McpProperties mcpProperties;
|
||||
|
||||
private volatile Map<String, RegisteredTool> tools = Map.of();
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void initialize() {
|
||||
Map<String, RegisteredTool> discovered = new LinkedHashMap<>();
|
||||
|
||||
for (Object bean : applicationContext.getBeansOfType(Object.class).values()) {
|
||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||
for (Method declaredMethod : targetClass.getDeclaredMethods()) {
|
||||
McpTool annotation = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class);
|
||||
if (annotation == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisteredTool tool = new RegisteredTool(
|
||||
bean,
|
||||
findInvocableMethod(bean, declaredMethod),
|
||||
annotation,
|
||||
AnnotationUtils.findAnnotation(declaredMethod, ToolHint.class));
|
||||
register(discovered, annotation.name(), tool);
|
||||
registerNamespaceAlias(discovered, annotation.name(), tool);
|
||||
}
|
||||
}
|
||||
|
||||
tools = Map.copyOf(discovered);
|
||||
log.info("[Tool Registry] {} executable tool names cached", tools.size());
|
||||
}
|
||||
|
||||
public RegisteredTool find(String toolName) {
|
||||
return tools.get(toolName);
|
||||
}
|
||||
|
||||
private void registerNamespaceAlias(Map<String, RegisteredTool> discovered, String toolName,
|
||||
RegisteredTool tool) {
|
||||
String namespace = mcpProperties.getNamespace();
|
||||
if (StringUtils.hasText(namespace)) {
|
||||
register(discovered, namespace + "_" + toolName, tool);
|
||||
}
|
||||
}
|
||||
|
||||
private void register(Map<String, RegisteredTool> discovered, String toolName, RegisteredTool tool) {
|
||||
RegisteredTool existing = discovered.putIfAbsent(toolName, tool);
|
||||
if (existing != null && existing != tool) {
|
||||
throw new IllegalStateException("Duplicate @McpTool name: " + toolName);
|
||||
}
|
||||
}
|
||||
|
||||
private Method findInvocableMethod(Object bean, Method declaredMethod) {
|
||||
try {
|
||||
return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes());
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
return declaredMethod;
|
||||
}
|
||||
}
|
||||
|
||||
public record RegisteredTool(Object bean, Method method, McpTool annotation, ToolHint hint) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/** Protocol-neutral result of invoking one Tool Pod business tool. */
|
||||
public record ToolExecutionResult(int statusCode, Object body, Map<String, String> headers) {
|
||||
|
||||
public ToolExecutionResult {
|
||||
headers = headers == null ? Map.of() : Map.copyOf(headers);
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return statusCode >= 200 && statusCode < 300;
|
||||
}
|
||||
}
|
||||
@@ -5,33 +5,29 @@ import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
|
||||
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
|
||||
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
|
||||
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
|
||||
@Component
|
||||
@ConditionalOnBean(BusinessToolController.class)
|
||||
@ConditionalOnBean(McpToolExecutionService.class)
|
||||
public class ToolPodMcpToolSynchronizer {
|
||||
private final McpSyncServer mcpServer;
|
||||
private final ToolRegistryHeartbeatSender heartbeatSender;
|
||||
private final BusinessToolController businessToolController;
|
||||
private final McpToolExecutionService toolExecutionService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender,
|
||||
BusinessToolController businessToolController, ObjectMapper objectMapper) {
|
||||
McpToolExecutionService toolExecutionService, ObjectMapper objectMapper) {
|
||||
this.mcpServer = mcpServer;
|
||||
this.heartbeatSender = heartbeatSender;
|
||||
this.businessToolController = businessToolController;
|
||||
this.toolExecutionService = toolExecutionService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -46,12 +42,14 @@ public class ToolPodMcpToolSynchronizer {
|
||||
McpSchema.Tool mcpTool = McpSchema.Tool.builder()
|
||||
.name(tool.getName())
|
||||
.description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription())
|
||||
.inputSchema(tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema())
|
||||
.annotations(McpSchema.ToolAnnotations.builder()
|
||||
.readOnlyHint(Boolean.TRUE.equals(tool.getReadOnlyHint()))
|
||||
.destructiveHint(Boolean.TRUE.equals(tool.getDestructiveHint()))
|
||||
.idempotentHint(Boolean.TRUE.equals(tool.getIdempotentHint()))
|
||||
.openWorldHint(Boolean.TRUE.equals(tool.getOpenWorldHint())).build())
|
||||
.inputSchema(toJsonSchema(tool.getParametersSchema()))
|
||||
.annotations(new McpSchema.ToolAnnotations(
|
||||
tool.getDisplayName(),
|
||||
tool.getReadOnlyHint(),
|
||||
tool.getDestructiveHint(),
|
||||
tool.getIdempotentHint(),
|
||||
tool.getOpenWorldHint(),
|
||||
null))
|
||||
.build();
|
||||
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
|
||||
.callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build();
|
||||
@@ -59,15 +57,9 @@ public class ToolPodMcpToolSynchronizer {
|
||||
|
||||
private McpSchema.CallToolResult invoke(String toolName, McpRequestHeaders requestHeaders,
|
||||
Map<String, Object> arguments) {
|
||||
ResponseEntity<?> response = businessToolController.executeDynamicTool(
|
||||
toolName,
|
||||
requestHeaders == null ? null : requestHeaders.headerRequestId(),
|
||||
requestHeaders == null ? null : requestHeaders.traceId(),
|
||||
requestHeaders == null ? null : requestHeaders.requestId(),
|
||||
requestHeaders == null ? null : requestHeaders.encryptedEmployeeId(),
|
||||
arguments);
|
||||
boolean failed = !response.getStatusCode().is2xxSuccessful();
|
||||
Object body = response.getBody();
|
||||
ToolExecutionResult result = toolExecutionService.execute(toolName, requestHeaders, arguments);
|
||||
boolean failed = !result.isSuccess();
|
||||
Object body = result.body();
|
||||
try {
|
||||
return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body))
|
||||
.structuredContent(body).isError(failed).build();
|
||||
@@ -83,4 +75,19 @@ public class ToolPodMcpToolSynchronizer {
|
||||
schema.put("additionalProperties", false);
|
||||
return schema;
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
private McpSchema.JsonSchema toJsonSchema(Map<String, Object> source) {
|
||||
Map<String, Object> schema = source == null ? emptySchema() : source;
|
||||
return new McpSchema.JsonSchema(
|
||||
String.valueOf(schema.getOrDefault("type", "object")),
|
||||
schema.get("properties") instanceof Map<?, ?> properties
|
||||
? (Map<String, Object>) properties : Map.of(),
|
||||
schema.get("required") instanceof List<?> required
|
||||
? (List<String>) required : List.of(),
|
||||
schema.get("additionalProperties") instanceof Boolean additionalProperties
|
||||
? additionalProperties : Boolean.TRUE,
|
||||
schema.get("$defs") instanceof Map<?, ?> defs ? (Map<String, Object>) defs : Map.of(),
|
||||
schema.get("definitions") instanceof Map<?, ?> definitions
|
||||
? (Map<String, Object>) definitions : Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ package io.shinhanlife.dap.lib.mcp;
|
||||
* </pre>
|
||||
*/
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.ai.mcp.annotation.McpTool;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
@@ -44,14 +44,13 @@ import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(BusinessToolController.class)
|
||||
@ConditionalOnBean(McpToolExecutionService.class)
|
||||
public class ToolRegistryHeartbeatSender {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
@@ -92,10 +91,13 @@ public class ToolRegistryHeartbeatSender {
|
||||
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
||||
: rawSubToolName;
|
||||
|
||||
boolean isRegister = hintAnnotation != null && hintAnnotation.register();
|
||||
// @ToolHint(register = false)인 Tool은 메타데이터 조회에는 남기되,
|
||||
// Gateway 등록 및 heartbeat 전송 대상에서는 제외합니다.
|
||||
// ToolHint가 없는 기존 Tool은 이전 동작과 동일하게 등록합니다.
|
||||
boolean isRegister = hintAnnotation == null || hintAnnotation.register();
|
||||
if (!isRegister) {
|
||||
log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName);
|
||||
log.info(" [HeartbeatSender] '{}' Tool is excluded from Gateway registration because register=false. (tool name: {})",
|
||||
baseName, subToolName);
|
||||
}
|
||||
|
||||
ToolMetadata meta = new ToolMetadata();
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||
import org.springaicommunity.mcp.annotation.McpToolParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
@@ -76,7 +76,8 @@ public class JsonSchemaGenerator {
|
||||
if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) {
|
||||
fieldSchema.put("description", schemaAnnotation.description());
|
||||
}
|
||||
if (schemaAnnotation.required() && !requiredList.contains(field.getName())) {
|
||||
if ((schemaAnnotation.required() || schemaAnnotation.requiredMode() == Schema.RequiredMode.REQUIRED)
|
||||
&& !requiredList.contains(field.getName())) {
|
||||
requiredList.add(field.getName());
|
||||
}
|
||||
if (!schemaAnnotation.pattern().isEmpty()) {
|
||||
|
||||
@@ -46,7 +46,11 @@ public class PodScaffolder {
|
||||
public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException {
|
||||
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||
|
||||
return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate);
|
||||
}
|
||||
|
||||
static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName,
|
||||
String author, String createDate) throws IOException {
|
||||
Path modulePath = rootDir.resolve(Paths.get(moduleName));
|
||||
if (Files.exists(modulePath)) {
|
||||
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
|
||||
@@ -59,25 +63,27 @@ public class PodScaffolder {
|
||||
log.append("[2/6] build.gradle 생성 중...\n");
|
||||
String buildGradle = """
|
||||
plugins {
|
||||
// Spring Boot 3.5.11 version is managed by the root build.gradle.
|
||||
id 'org.springframework.boot'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Shared MCP, Glow integration, validation, logging, Lombok and MapStruct configuration.
|
||||
implementation project(':dap-was-lib')
|
||||
}
|
||||
dependencies {
|
||||
compileOnly 'org.projectlombok:lombok:1.18.32'
|
||||
annotationProcessor 'org.projectlombok:lombok:1.18.32'
|
||||
}
|
||||
""";
|
||||
Files.writeString(modulePath.resolve("build.gradle"), buildGradle);
|
||||
|
||||
log.append("[3/6] Dockerfile 생성 중...\n");
|
||||
String dockerfile = """
|
||||
FROM eclipse-temurin:21-jdk-alpine
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ=Asia/Seoul
|
||||
COPY %s/build/libs/*-SNAPSHOT.jar app.jar
|
||||
EXPOSE %s
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
""".formatted(moduleName);
|
||||
""".formatted(moduleName, portStr);
|
||||
Files.writeString(modulePath.resolve("Dockerfile"), dockerfile);
|
||||
|
||||
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
|
||||
@@ -132,15 +138,24 @@ public class PodScaffolder {
|
||||
org.apache.kafka: ERROR
|
||||
mcp:
|
||||
namespace: ""
|
||||
manifest:
|
||||
bundle-id: %s
|
||||
name-prefix: ""
|
||||
security:
|
||||
tenant-domains:
|
||||
TESTER-DEV: ALL
|
||||
""".formatted(portStr, moduleName);
|
||||
""".formatted(portStr, moduleName, moduleName.replace("dap-", ""));
|
||||
Files.writeString(resPath.resolve("application.yml"), applicationYml);
|
||||
|
||||
String applicationLocalYml = """
|
||||
# Local 환경 전용 설정 (H2 메모리 DB 등)
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
import:
|
||||
- classpath:glow/application-glow.yml
|
||||
- classpath:glow/application-glow-local.yml
|
||||
datasource:
|
||||
url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
|
||||
driverClassName: com.p6spy.engine.spy.P6SpyDriver
|
||||
@@ -186,6 +201,14 @@ public class PodScaffolder {
|
||||
server:
|
||||
port: ${PORT:%s}
|
||||
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
import:
|
||||
- classpath:glow/application-glow.yml
|
||||
- classpath:glow/application-glow-dev.yml
|
||||
|
||||
axhub:
|
||||
gateway:
|
||||
url: https://axhubmcp.devjun.net
|
||||
@@ -223,6 +246,46 @@ public class PodScaffolder {
|
||||
""".formatted(portStr, portStr);
|
||||
Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml);
|
||||
|
||||
String applicationTestYml = """
|
||||
server:
|
||||
port: ${PORT:%s}
|
||||
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: test
|
||||
import:
|
||||
- classpath:glow/application-glow.yml
|
||||
- classpath:glow/application-glow-test.yml
|
||||
|
||||
axhub:
|
||||
gateway:
|
||||
url: ${AXHUB_GATEWAY_URL}
|
||||
tool:
|
||||
url: ${AXHUB_TOOL_URL}
|
||||
""".formatted(portStr);
|
||||
Files.writeString(resPath.resolve("application-test.yml"), applicationTestYml);
|
||||
|
||||
String applicationProdYml = """
|
||||
server:
|
||||
port: ${PORT:%s}
|
||||
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
import:
|
||||
- classpath:glow/application-glow.yml
|
||||
- classpath:glow/application-glow-prod.yml
|
||||
|
||||
axhub:
|
||||
gateway:
|
||||
url: ${AXHUB_GATEWAY_URL}
|
||||
tool:
|
||||
url: ${AXHUB_TOOL_URL}
|
||||
""".formatted(portStr);
|
||||
Files.writeString(resPath.resolve("application-prod.yml"), applicationProdYml);
|
||||
|
||||
String logbackXml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
package io.shinhanlife.dap.lib.util;
|
||||
|
||||
|
||||
import org.springframework.ai.mcp.annotation.McpToolParam;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
@@ -42,6 +44,9 @@ public class ToolScaffolder {
|
||||
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
|
||||
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
|
||||
|
||||
public record FieldDefinition(String name, String type, String description, String example, boolean required) {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
@@ -74,10 +79,11 @@ public class ToolScaffolder {
|
||||
String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): ");
|
||||
boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim());
|
||||
|
||||
String inputSchemaResource = useSchemaResource ? "classpath:mcp/schema/" + toKebabCase(baseName) + "-resource-input-schema.json" : null;
|
||||
String outputSchemaResource = useSchemaResource ? "classpath:mcp/schema/" + toKebabCase(baseName) + "-resource-output-schema.json" : null;
|
||||
String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/";
|
||||
String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null;
|
||||
String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null;
|
||||
|
||||
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, inputSchemaResource, outputSchemaResource);
|
||||
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
@@ -94,6 +100,12 @@ public class ToolScaffolder {
|
||||
}
|
||||
|
||||
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource) throws IOException {
|
||||
return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate,
|
||||
register, clientSystemCode, inputSchemaResource, outputSchemaResource,
|
||||
List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of());
|
||||
}
|
||||
|
||||
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List<FieldDefinition> inputFields, List<FieldDefinition> outputFields) throws IOException {
|
||||
baseName = toPascalCase(baseName);
|
||||
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||
@@ -110,22 +122,20 @@ public class ToolScaffolder {
|
||||
String schemaBaseName = toKebabCase(baseName);
|
||||
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
|
||||
String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json";
|
||||
Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mcp/schema"));
|
||||
String inputSchemaClasspath = "classpath:mcp/schema/" + inputSchemaFileName;
|
||||
String outputSchemaClasspath = "classpath:mcp/schema/" + outputSchemaFileName;
|
||||
Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/tool-schemas", group.toLowerCase()));
|
||||
String inputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + inputSchemaFileName;
|
||||
String outputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + outputSchemaFileName;
|
||||
|
||||
String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase();
|
||||
boolean isMci = "MCI".equalsIgnoreCase(routingType);
|
||||
String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase();
|
||||
String clientPkgSuffix = "";
|
||||
String clientPrefixCap = "";
|
||||
Path mciClientDir = null;
|
||||
|
||||
if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) {
|
||||
String clientPrefix = clientSystemCode.toLowerCase();
|
||||
clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4);
|
||||
clientPrefixCap = toPascalCase(clientSystemCode);
|
||||
mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4);
|
||||
mciGroupPath = "infra/itrf/mci/" + clientPrefix;
|
||||
mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath));
|
||||
}
|
||||
|
||||
@@ -178,6 +188,14 @@ public class ToolScaffolder {
|
||||
private String message;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
reqContent = reqContent
|
||||
.replace("import com.fasterxml.jackson.annotation.JsonInclude;",
|
||||
"import com.fasterxml.jackson.annotation.JsonInclude;\nimport io.swagger.v3.oas.annotations.media.Schema;")
|
||||
.replaceAll("(?m)^\\s*@McpToolParam\\([^\\r\\n]*\\)\\R", "")
|
||||
.replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "")
|
||||
.replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;")
|
||||
.replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;");
|
||||
reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent);
|
||||
|
||||
// Generate Response DTO
|
||||
@@ -211,23 +229,24 @@ public class ToolScaffolder {
|
||||
// TODO: Add response fields here. Do not include PII in the Tool response.
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
|
||||
|
||||
String toolName = toToolName(moduleName, group, baseName);
|
||||
|
||||
String toolHintLine;
|
||||
if (useSchemaResource) {
|
||||
toolHintLine = " @ToolHint(register = %s,\n" +
|
||||
" inputSchemaResource = \"%s\",\n" +
|
||||
" outputSchemaResource = \"%s\")".formatted(register, inputSchemaClasspath, outputSchemaClasspath);
|
||||
toolHintLine = (" @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" +
|
||||
" inputSchemaResource = \"%s\",\n" +
|
||||
" outputSchemaResource = \"%s\")").formatted(register, group.toLowerCase(Locale.ROOT), interfaceId, inputSchemaClasspath, outputSchemaClasspath);
|
||||
} else {
|
||||
toolHintLine = " @ToolHint(register = %s)".formatted(register);
|
||||
toolHintLine = " @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId);
|
||||
}
|
||||
|
||||
String serviceInterfaceContent = """
|
||||
package %s.usecase;
|
||||
|
||||
import org.springframework.ai.mcp.annotation.McpTool;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
@@ -279,7 +298,6 @@ public class ToolScaffolder {
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Map;
|
||||
import %s.converter.%sConverter;
|
||||
import %s.%s.io.%s_I;
|
||||
%s
|
||||
@@ -307,7 +325,7 @@ public class ToolScaffolder {
|
||||
private final %sConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(%sRequest req) {
|
||||
public %sResponse execute(%sRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신.", "%s");
|
||||
try {
|
||||
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||
@@ -319,10 +337,18 @@ public class ToolScaffolder {
|
||||
mciReq,
|
||||
Object.class
|
||||
);
|
||||
return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS");
|
||||
%sResponse response = new %sResponse();
|
||||
response.setResultCode("SUCCESS");
|
||||
response.setResultMessage(resTransfer.getBody() != null
|
||||
? "MCI call completed."
|
||||
: "MCI call completed without a response body.");
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||
%sResponse response = new %sResponse();
|
||||
response.setResultCode("ERROR");
|
||||
response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,12 +370,27 @@ public class ToolScaffolder {
|
||||
(clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"),
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
toolName,
|
||||
interfaceId,
|
||||
interfaceId
|
||||
interfaceId,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName
|
||||
);
|
||||
String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", ".");
|
||||
serviceImplContent = serviceImplContent
|
||||
.replace("import " + mciIoPackage + ".io." + interfaceId + "_I;",
|
||||
"import " + mciIoPackage + ".io." + interfaceId + "_I;\nimport " + mciIoPackage + ".io." + interfaceId + "_O;")
|
||||
.replace("Transfer<Object> resTransfer", "Transfer<" + interfaceId + "_O> resTransfer")
|
||||
.replace("Object.class", interfaceId + "_O.class")
|
||||
.replace("response.setResultCode(\"SUCCESS\");",
|
||||
"if (resTransfer.getBody() != null) {\n response = converter.toResponse(resTransfer.getBody());\n }\n response.setResultCode(\"SUCCESS\");");
|
||||
} else {
|
||||
serviceImplContent = """
|
||||
serviceImplContent = "HTTP".equalsIgnoreCase(routingType)
|
||||
? httpUseCaseImplContent(bizPackage, baseName, interfaceId, author, createDate)
|
||||
: """
|
||||
package %s.usecase.impl;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
@@ -383,9 +424,16 @@ public class ToolScaffolder {
|
||||
private final %sConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(%sRequest req) {
|
||||
public %sResponse execute(%sRequest req) {
|
||||
// %sLegacyRequest legacyRequest = converter.toLegacyRequest(req);
|
||||
return executeLegacy("%s", "%s", req); // Or pass legacyRequest
|
||||
Object legacyResponse = executeLegacy("%s", "%s", req); // Or pass legacyRequest
|
||||
if (legacyResponse instanceof %sResponse response) {
|
||||
return response;
|
||||
}
|
||||
%sResponse response = new %sResponse();
|
||||
response.setResultCode("SUCCESS");
|
||||
response.setResultMessage("Legacy call completed.");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
@@ -402,6 +450,10 @@ public class ToolScaffolder {
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
baseName,
|
||||
routingType, interfaceId
|
||||
);
|
||||
}
|
||||
@@ -441,6 +493,7 @@ public class ToolScaffolder {
|
||||
private String content;
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||
mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate);
|
||||
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
|
||||
|
||||
String mciResContent = """
|
||||
@@ -467,6 +520,7 @@ public class ToolScaffolder {
|
||||
// TODO: Add response fields here
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
|
||||
mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate);
|
||||
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
|
||||
|
||||
String converterContent = """
|
||||
@@ -518,6 +572,7 @@ public class ToolScaffolder {
|
||||
baseName, interfaceId,
|
||||
baseName, interfaceId
|
||||
);
|
||||
converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId);
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
@@ -559,7 +614,7 @@ public class ToolScaffolder {
|
||||
public class Mci%sClient {
|
||||
private final AxhubMciComponent mci;
|
||||
|
||||
public Transfer<Object> callTo(String interfaceId, String dummy, Object mciReq, Class<Object> resType) throws Exception {
|
||||
public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> resType) throws Exception {
|
||||
return mci.callTo(interfaceId, dummy, mciReq, resType);
|
||||
}
|
||||
}
|
||||
@@ -604,6 +659,7 @@ public class ToolScaffolder {
|
||||
private String content;
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
legacyReqContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyRequest", inputFields, author, createDate, true);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
|
||||
|
||||
String legacyResContent = """
|
||||
@@ -630,6 +686,7 @@ public class ToolScaffolder {
|
||||
// TODO: Add legacy response fields here
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
|
||||
legacyResContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyResponse", outputFields, author, createDate, true);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
|
||||
|
||||
String converterContent = """
|
||||
@@ -679,6 +736,7 @@ public class ToolScaffolder {
|
||||
bizPackage, baseName, author, createDate, createDate, author,
|
||||
baseName, baseName, baseName, baseName, baseName, baseName, baseName
|
||||
);
|
||||
converterContent = legacyConverterContent(bizPackage, baseName);
|
||||
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
@@ -732,6 +790,17 @@ public class ToolScaffolder {
|
||||
log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n");
|
||||
}
|
||||
|
||||
Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json"));
|
||||
Files.createDirectories(mockResponsePath.getParent());
|
||||
Files.writeString(mockResponsePath, mockResponseContent(outputFields));
|
||||
|
||||
Path generatedTestDir = rootDir.resolve(Paths.get(moduleName, "src/test/java/io/shinhanlife/dap/mcc/biz", group.toLowerCase(), "usecase"));
|
||||
Files.createDirectories(generatedTestDir);
|
||||
Path generatedTestPath = generatedTestDir.resolve(baseName + "UseCaseTest.java");
|
||||
Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName));
|
||||
log.append("[Mock Response] ").append(mockResponsePath).append("\\n");
|
||||
log.append("[Unit Test] ").append(generatedTestPath).append("\\n");
|
||||
log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n");
|
||||
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
|
||||
|
||||
return log.toString();
|
||||
@@ -744,6 +813,223 @@ public class ToolScaffolder {
|
||||
.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static String dtoContent(String packageName, String className, List<FieldDefinition> fields,
|
||||
String author, String createDate, boolean request) {
|
||||
String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage"));
|
||||
if (!request) {
|
||||
body = " private String resultCode;\n\n private String resultMessage;\n" + body;
|
||||
}
|
||||
return """
|
||||
package %s;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %s {
|
||||
%s}
|
||||
""".formatted(packageName, className, body);
|
||||
}
|
||||
|
||||
private static String mciIoContent(String packageSuffix, String className, List<FieldDefinition> fields,
|
||||
String author, String createDate) {
|
||||
return """
|
||||
package %s.%s.io;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class %s {
|
||||
%s}
|
||||
""".formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields));
|
||||
}
|
||||
|
||||
private static String httpUseCaseImplContent(String bizPackage, String baseName, String interfaceId, String author, String createDate) {
|
||||
return """
|
||||
package %s.usecase.impl;
|
||||
|
||||
import %s.converter.%sConverter;
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.legacy.%sLegacyRequest;
|
||||
import %s.legacy.%sLegacyResponse;
|
||||
import %s.usecase.%sUseCase;
|
||||
import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent;
|
||||
import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpDomain;
|
||||
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* HTTP Tool implementation. Calls the Glow HTTP adapter through AxhubHttpComponent.
|
||||
* Configure the target domain in AxhubHttpDomain and glow.communication.http.api-list before use.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase {
|
||||
|
||||
private final %sConverter converter;
|
||||
private final AxhubHttpComponent http;
|
||||
|
||||
@Override
|
||||
public %sResponse execute(%sRequest req) {
|
||||
%sLegacyRequest legacyRequest = converter.toLegacyRequest(req);
|
||||
%sLegacyResponse legacyResponse = http.call(
|
||||
AxhubHttpDomain.SAMPLE,
|
||||
"/%s",
|
||||
legacyRequest,
|
||||
%sLegacyResponse.class
|
||||
);
|
||||
|
||||
%sResponse response = converter.toResponse(legacyResponse);
|
||||
response.setResultCode("SUCCESS");
|
||||
response.setResultMessage("HTTP API call completed.");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage, bizPackage, baseName, bizPackage, baseName, bizPackage, baseName,
|
||||
bizPackage, baseName, bizPackage, baseName, bizPackage, baseName,
|
||||
baseName, baseName, baseName, baseName, baseName, baseName, baseName,
|
||||
interfaceId, baseName, baseName);
|
||||
}
|
||||
private static String mciConverterContent(String bizPackage, String baseName, String mciPackage, String interfaceId) {
|
||||
return """
|
||||
package %s.converter;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.%s.io.%s_I;
|
||||
import %s.%s.io.%s_O;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface %sConverter {
|
||||
%s_I toLegacyRequest(%sRequest request);
|
||||
%sRequest toRequest(%s_I mciRequest);
|
||||
%sResponse toResponse(%s_O mciRes);
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, bizPackage, baseName,
|
||||
BASE_PACKAGE, mciPackage, interfaceId, BASE_PACKAGE, mciPackage, interfaceId,
|
||||
baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId);
|
||||
}
|
||||
|
||||
private static String legacyConverterContent(String bizPackage, String baseName) {
|
||||
return """
|
||||
package %s.converter;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import %s.legacy.%sLegacyRequest;
|
||||
import %s.legacy.%sLegacyResponse;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface %sConverter {
|
||||
%sLegacyRequest toLegacyRequest(%sRequest request);
|
||||
%sRequest toRequest(%sLegacyRequest legacyRequest);
|
||||
%sResponse toResponse(%sLegacyResponse legacyResponse);
|
||||
}
|
||||
""".formatted(
|
||||
bizPackage, bizPackage, baseName, bizPackage, baseName,
|
||||
bizPackage, baseName, bizPackage, baseName,
|
||||
baseName, baseName, baseName, baseName, baseName, baseName, baseName);
|
||||
}
|
||||
private static String fieldLines(List<FieldDefinition> fields) {
|
||||
return fieldLines(fields, Set.of());
|
||||
}
|
||||
|
||||
private static String fieldLines(List<FieldDefinition> fields, Set<String> excludedNames) {
|
||||
StringBuilder source = new StringBuilder();
|
||||
Set<String> generatedNames = new LinkedHashSet<>();
|
||||
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
|
||||
if (field == null || field.name() == null || field.name().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String fieldName = field.name().trim();
|
||||
if (excludedNames.contains(fieldName) || !generatedNames.add(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
String type = supportedType(field.type());
|
||||
String description = field.description() == null ? "" : field.description().replace("\"", "\\\"");
|
||||
String example = field.example() == null ? "" : field.example().replace("\"", "\\\"");
|
||||
source.append(" @Schema(description = \"").append(description).append("\", example = \"")
|
||||
.append(example).append("\"");
|
||||
if (field.required()) {
|
||||
source.append(", requiredMode = Schema.RequiredMode.REQUIRED");
|
||||
}
|
||||
source.append(")\n private ").append(type).append(' ').append(fieldName).append(";\n\n");
|
||||
}
|
||||
return source.toString();
|
||||
}
|
||||
private static String supportedType(String type) {
|
||||
return switch (type == null ? "String" : type) {
|
||||
case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type;
|
||||
default -> throw new IllegalArgumentException("Unsupported field type: " + type);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private static String mockResponseContent(List<FieldDefinition> outputFields) {
|
||||
StringBuilder json = new StringBuilder("{\n");
|
||||
List<FieldDefinition> fields = outputFields == null ? List.of() : outputFields;
|
||||
boolean first = true;
|
||||
for (FieldDefinition field : fields) {
|
||||
if (field.name() == null || field.name().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
json.append(",\n");
|
||||
}
|
||||
json.append(" \"").append(jsonEscape(field.name())).append("\" : ")
|
||||
.append(mockValue(field));
|
||||
first = false;
|
||||
}
|
||||
return json.append("\n}\n").toString();
|
||||
}
|
||||
|
||||
private static String mockValue(FieldDefinition field) {
|
||||
if (field.example() == null || field.example().isBlank()) {
|
||||
return "null";
|
||||
}
|
||||
return switch (supportedType(field.type())) {
|
||||
case "Integer", "Long", "Double", "BigDecimal" -> field.example();
|
||||
case "Boolean" -> Boolean.parseBoolean(field.example()) ? "true" : "false";
|
||||
default -> "\"" + jsonEscape(field.example()) + "\"";
|
||||
};
|
||||
}
|
||||
|
||||
private static String jsonEscape(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
private static String useCaseTestContent(String bizPackage, String baseName) {
|
||||
return """
|
||||
package %s.usecase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import %s.dto.%sRequest;
|
||||
import %s.dto.%sResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class %sUseCaseTest {
|
||||
|
||||
@Test
|
||||
void createsToolRequestAndResponseDtos() {
|
||||
assertNotNull(new %sRequest());
|
||||
assertNotNull(new %sResponse());
|
||||
}
|
||||
}
|
||||
""".formatted(bizPackage, bizPackage, baseName, bizPackage, baseName,
|
||||
baseName, baseName, baseName);
|
||||
}
|
||||
private static String toToolName(String moduleName, String group, String baseName) {
|
||||
String moduleDirectory = Path.of(moduleName).getFileName().toString();
|
||||
String pod = moduleDirectory.startsWith("dap-was-")
|
||||
@@ -756,7 +1042,7 @@ public class ToolScaffolder {
|
||||
String[] words = normalizedName.split("\\s+");
|
||||
String service = words[0];
|
||||
String action = words.length == 1 ? "execute" : words[words.length - 1];
|
||||
return "%s.%s.%s.%s".formatted(
|
||||
return "%s_%s_%s_%s".formatted(
|
||||
pod.toLowerCase(Locale.ROOT),
|
||||
group.toLowerCase(Locale.ROOT),
|
||||
service,
|
||||
@@ -784,4 +1070,4 @@ public class ToolScaffolder {
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package io.shinhanlife.dap.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
// removed McpOutputSchema
|
||||
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
|
||||
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import io.shinhanlife.dap.lib.annotation.ToolHint;
|
||||
|
||||
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
|
||||
/** Resolves MCP Tool schemas from resources or DTO metadata. */
|
||||
public class ToolSchemaResolver {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
@@ -18,7 +17,8 @@ public class ToolSchemaResolver {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public Map<String, Object> resolve(org.springframework.ai.mcp.annotation.McpTool function, ToolHint hint, Class<?> requestType) {
|
||||
public Map<String, Object> resolve(org.springaicommunity.mcp.annotation.McpTool function,
|
||||
ToolHint hint, Class<?> requestType) {
|
||||
if (hint != null && !hint.inputSchemaResource().isBlank()) {
|
||||
return loadResource(hint.inputSchemaResource());
|
||||
}
|
||||
@@ -26,12 +26,11 @@ public class ToolSchemaResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an explicitly declared response schema.
|
||||
* Response schemas are opt-in so existing tools keep their current response behavior.
|
||||
* ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고,
|
||||
* 없으면 responseType DTO를 분석하여 자동 생성합니다.
|
||||
* Resolves a response schema only when it is explicitly declared.
|
||||
* A JSON resource has precedence over a DTO marker annotation.
|
||||
*/
|
||||
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class<?> responseType, ToolHint hint) {
|
||||
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function,
|
||||
Class<?> responseType, ToolHint hint) {
|
||||
if (hint != null && !hint.outputSchemaResource().isBlank()) {
|
||||
return loadResource(hint.outputSchemaResource());
|
||||
}
|
||||
@@ -39,16 +38,16 @@ public class ToolSchemaResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an explicitly declared response schema.
|
||||
* Response schemas are opt-in so existing tools keep their current response behavior.
|
||||
* Generates a response schema only for DTOs marked with {@link McpOutputSchema}.
|
||||
*/
|
||||
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class<?> responseType) {
|
||||
// Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵
|
||||
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function,
|
||||
Class<?> responseType) {
|
||||
if (responseType == null
|
||||
|| responseType == Object.class
|
||||
|| Map.class.isAssignableFrom(responseType)
|
||||
|| responseType == Void.class
|
||||
|| responseType == void.class) {
|
||||
|| responseType == void.class
|
||||
|| !responseType.isAnnotationPresent(McpOutputSchema.class)) {
|
||||
return Map.of();
|
||||
}
|
||||
return JsonSchemaGenerator.generateSchema(responseType);
|
||||
@@ -57,8 +56,8 @@ public class ToolSchemaResolver {
|
||||
/**
|
||||
* Retained for callers that use only explicit output schemas.
|
||||
*/
|
||||
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) {
|
||||
return resolveOutput(function, null);
|
||||
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private Map<String, Object> loadResource(String location) {
|
||||
@@ -76,12 +75,4 @@ public class ToolSchemaResolver {
|
||||
throw new IllegalStateException("Failed to load MCP schema resource: " + location, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> parse(String schema, String source) {
|
||||
try {
|
||||
return objectMapper.readValue(schema, new TypeReference<>() { });
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to parse MCP input schema from " + source, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import java.util.regex.Pattern;
|
||||
public final class McpToolNameValidator {
|
||||
|
||||
private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\"");
|
||||
private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$");
|
||||
private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-zA-Z0-9_-]{1,128}$");
|
||||
|
||||
private McpToolNameValidator() {
|
||||
}
|
||||
@@ -139,7 +139,7 @@ public final class McpToolNameValidator {
|
||||
}
|
||||
|
||||
private static String buildInvalidNameMessage(List<Map.Entry<String, List<ToolDeclaration>>> invalidNames) {
|
||||
StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected pod.domain.service.action using lowercase letters, digits, or hyphens.");
|
||||
StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected 1-128 characters using letters, digits, underscores, or hyphens.");
|
||||
for (Map.Entry<String, List<ToolDeclaration>> invalidName : invalidNames) {
|
||||
message.append("\n\n").append(invalidName.getKey());
|
||||
invalidName.getValue().stream()
|
||||
|
||||
@@ -8,10 +8,8 @@ import com.networknt.schema.SchemaRegistry;
|
||||
import com.networknt.schema.SpecificationVersion;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Validates tool arguments with the NetworkNT version selected by the MCP SDK. */
|
||||
@Component
|
||||
public class ToolArgumentSchemaValidator {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@@ -1,42 +1,14 @@
|
||||
package io.shinhanlife.dap.mcc.presentation;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcc.presentation
|
||||
* @className BusinessToolController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.networknt.schema.Error;
|
||||
import org.springframework.ai.mcp.annotation.McpTool;
|
||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||
import io.shinhanlife.dap.lib.config.McpProperties;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
|
||||
import io.shinhanlife.dap.lib.mcp.ToolExecutionResult;
|
||||
import io.shinhanlife.dap.lib.mcp.McpToolExecutionService;
|
||||
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
|
||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -45,26 +17,20 @@ import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Slf4j
|
||||
/** Legacy REST adapter for Tool Pod execution. */
|
||||
@RestController
|
||||
@RequestMapping("/")
|
||||
@RequiredArgsConstructor
|
||||
public class BusinessToolController {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final McpProperties mcpProperties;
|
||||
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
|
||||
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
|
||||
private final McpToolExecutionService toolExecutionService;
|
||||
|
||||
private final ToolSchemaResolver toolSchemaResolver;
|
||||
// 내부 조회용 로컬 Tool 목록 엔드포인트
|
||||
@GetMapping("/mcp/api/v1/tools/local")
|
||||
public List<ToolMetadata> getLocalTools() {
|
||||
return toolRegistryHeartbeatSender.getAllScannedTools();
|
||||
}
|
||||
|
||||
// 순수 REST 기반 동적 라우팅 엔드포인트
|
||||
@PostMapping("/mcp/{name}")
|
||||
public ResponseEntity<?> executeDynamicTool(
|
||||
@PathVariable("name") String functionName,
|
||||
@@ -73,173 +39,12 @@ public class BusinessToolController {
|
||||
@RequestHeader(value = "request-id", required = false) String requestId,
|
||||
@RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId,
|
||||
@RequestBody(required = false) Map<String, Object> arguments) {
|
||||
|
||||
String finalRequestId = headerRequestId;
|
||||
|
||||
log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId);
|
||||
log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
|
||||
if (arguments != null) {
|
||||
try {
|
||||
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments));
|
||||
} catch (Exception e) {
|
||||
log.info(" [Tool] 호출 파라미터: {}", arguments);
|
||||
}
|
||||
}
|
||||
Object targetBean = null;
|
||||
Method targetMethod = null;
|
||||
McpTool targetFunctionAnnotation = null;
|
||||
|
||||
// McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpTool을 찾습니다.
|
||||
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
|
||||
outerLoop:
|
||||
for (Object bean : allBeans.values()) {
|
||||
Class<?> targetClass = AopUtils.getTargetClass(bean);
|
||||
for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) {
|
||||
McpTool mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpTool.class);
|
||||
if (mcpFunc != null) {
|
||||
String baseName = mcpFunc.name();
|
||||
String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + baseName
|
||||
: baseName;
|
||||
|
||||
if (expectedName.equals(functionName) || baseName.equals(functionName)) {
|
||||
targetBean = bean;
|
||||
try {
|
||||
targetMethod = bean.getClass().getMethod(targetMethodOfClass.getName(), targetMethodOfClass.getParameterTypes());
|
||||
} catch (NoSuchMethodException e) {
|
||||
targetMethod = targetMethodOfClass;
|
||||
}
|
||||
targetFunctionAnnotation = mcpFunc;
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBean == null || targetMethod == null) {
|
||||
List<String> availableFunctions = new ArrayList<>();
|
||||
for (Object bean : allBeans.values()) {
|
||||
Class<?> targetCls = AopUtils.getTargetClass(bean);
|
||||
for (Method m : targetCls.getDeclaredMethods()) {
|
||||
McpTool func = AnnotationUtils.findAnnotation(m, McpTool.class);
|
||||
if (func != null) {
|
||||
String baseName = func.name();
|
||||
String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + baseName : baseName;
|
||||
availableFunctions.add(expName + " (in " + targetCls.getSimpleName() + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions);
|
||||
Map<String, Object> errorDetails = new HashMap<>();
|
||||
errorDetails.put("status", "404");
|
||||
Map<String, Object> errorBody = new HashMap<>();
|
||||
errorBody.put("code", "TOOL_NOT_FOUND");
|
||||
errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName);
|
||||
errorBody.put("details", errorDetails);
|
||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
||||
|
||||
return ResponseEntity.status(404).body(errorBody);
|
||||
}
|
||||
// (기존 차단 로직 제거됨)
|
||||
|
||||
// 2. 파라미터 유효성 검증 (JSON Schema)
|
||||
if (targetMethod.getParameterCount() > 0) {
|
||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||
if (!Map.class.isAssignableFrom(paramType)) {
|
||||
try {
|
||||
io.shinhanlife.dap.lib.annotation.ToolHint hint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class);
|
||||
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, hint, paramType);
|
||||
List<Error> errors = toolArgumentSchemaValidator.validate(inputSchema, arguments);
|
||||
if (!errors.isEmpty()) {
|
||||
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
|
||||
List<String> errorMessages = new ArrayList<>();
|
||||
for (Error validationError : errors) {
|
||||
errorMessages.add(validationError.getMessage());
|
||||
}
|
||||
Map<String, Object> errorDetails = new HashMap<>();
|
||||
errorDetails.put("status", "422");
|
||||
Map<String, Object> errorBody = new HashMap<>();
|
||||
errorBody.put("code", "INVALID_PARAM");
|
||||
errorBody.put("message", "파라미터 유효성 검증 실패");
|
||||
errorBody.put("details", errorDetails);
|
||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
||||
|
||||
return ResponseEntity.status(422).body(errorBody);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName());
|
||||
|
||||
try {
|
||||
// 3. DTO 파라미터 자동 매핑 (Map -> DTO)
|
||||
Object invokeArgument = arguments;
|
||||
if (targetMethod.getParameterCount() > 0 && arguments != null) {
|
||||
Class<?> paramType = targetMethod.getParameterTypes()[0];
|
||||
if (!Map.class.isAssignableFrom(paramType)) {
|
||||
invokeArgument = objectMapper.convertValue(arguments, paramType);
|
||||
log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Object methodResult = null;
|
||||
if (targetMethod.getParameterCount() == 0) {
|
||||
methodResult = targetMethod.invoke(targetBean);
|
||||
} else {
|
||||
methodResult = targetMethod.invoke(targetBean, invokeArgument);
|
||||
}
|
||||
|
||||
io.shinhanlife.dap.lib.annotation.ToolHint outputHint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class);
|
||||
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType(), outputHint);
|
||||
if (!outputSchema.isEmpty()) {
|
||||
List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
|
||||
if (!outputErrors.isEmpty()) {
|
||||
log.error("[Tool] Output schema validation failed. tool={}, errors={}",
|
||||
functionName, outputErrors);
|
||||
Map<String, Object> errorBody = new HashMap<>();
|
||||
errorBody.put("code", "INVALID_TOOL_RESPONSE");
|
||||
errorBody.put("message", "Tool response does not match its output schema");
|
||||
if (finalRequestId != null) {
|
||||
errorBody.put("request_id", finalRequestId);
|
||||
}
|
||||
return ResponseEntity.internalServerError().body(errorBody);
|
||||
}
|
||||
}
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
|
||||
// 5. 결과 반환 (순수 REST 응답)
|
||||
try {
|
||||
log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
|
||||
} catch (Exception e) {
|
||||
log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult);
|
||||
}
|
||||
|
||||
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
|
||||
|
||||
ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok();
|
||||
if (traceId != null) responseBuilder.header("trace-id", traceId);
|
||||
if (requestId != null) responseBuilder.header("request-id", requestId);
|
||||
|
||||
return responseBuilder.body(methodResult);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage());
|
||||
Map<String, Object> errorDetails = new HashMap<>();
|
||||
errorDetails.put("status", "500");
|
||||
Map<String, Object> errorBody = new HashMap<>();
|
||||
errorBody.put("code", "TOOL_ERROR");
|
||||
errorBody.put("message", "Tool execution failed");
|
||||
errorDetails.clear(); // Hide details for upstream errors
|
||||
errorBody.put("details", errorDetails);
|
||||
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
|
||||
|
||||
return ResponseEntity.status(502).body(errorBody);
|
||||
}
|
||||
ToolExecutionResult result = toolExecutionService.execute(
|
||||
functionName,
|
||||
new McpRequestHeaders(headerRequestId, traceId, requestId, encryptedEmployeeId),
|
||||
arguments);
|
||||
ResponseEntity.BodyBuilder response = ResponseEntity.status(result.statusCode());
|
||||
result.headers().forEach(response::header);
|
||||
return response.body(result.body());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.shinhanlife.glow.communication;
|
||||
|
||||
/** Minimal Glow communication contract used by the temporary compatibility layer. */
|
||||
public interface ICommunication<I, O> {
|
||||
O sync(I request);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package io.shinhanlife.glow.communication.module.http.component;
|
||||
|
||||
import io.shinhanlife.glow.communication.ICommunication;
|
||||
import io.shinhanlife.glow.communication.module.http.dto.HttpBody;
|
||||
import io.shinhanlife.glow.communication.module.http.dto.HttpHeader;
|
||||
import io.shinhanlife.glow.communication.module.http.dto.HttpTransfer;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
/**
|
||||
* Temporary compatibility implementation of the internal Glow HTTP component.
|
||||
* Replace this class with the official Glow HTTP JAR when it is supplied.
|
||||
*/
|
||||
@Component
|
||||
public class GlowHttpComponent implements ICommunication<HttpTransfer<?>, ResponseEntity<HttpBody>> {
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public GlowHttpComponent(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<HttpBody> sync(HttpTransfer<?> request) {
|
||||
if (request == null || request.getHeader() == null || request.getMethod() == null
|
||||
|| !StringUtils.hasText(request.getDomain())) {
|
||||
throw new IllegalArgumentException("Glow HTTP request header, domain, and method are required.");
|
||||
}
|
||||
if (HttpMethod.GET.equals(request.getMethod())) {
|
||||
return get(request);
|
||||
}
|
||||
if (HttpMethod.POST.equals(request.getMethod())) {
|
||||
return post(request);
|
||||
}
|
||||
if (HttpMethod.PUT.equals(request.getMethod())) {
|
||||
return put(request);
|
||||
}
|
||||
if (HttpMethod.DELETE.equals(request.getMethod())) {
|
||||
return delete(request);
|
||||
}
|
||||
throw new IllegalArgumentException("Unsupported HTTP method: " + request.getMethod());
|
||||
}
|
||||
|
||||
private ResponseEntity<HttpBody> get(HttpTransfer<?> request) {
|
||||
return toHttpBody(restClient.get().uri(buildUri(request, true))
|
||||
.headers(headers -> applyHeaders(headers, request.getHeader()))
|
||||
.retrieve().toEntity(String.class));
|
||||
}
|
||||
|
||||
private ResponseEntity<HttpBody> post(HttpTransfer<?> request) {
|
||||
return toHttpBody(restClient.post().uri(buildUri(request, false))
|
||||
.contentType(contentType(request)).headers(headers -> applyHeaders(headers, request.getHeader()))
|
||||
.body(request.getBody()).retrieve().toEntity(String.class));
|
||||
}
|
||||
|
||||
private ResponseEntity<HttpBody> put(HttpTransfer<?> request) {
|
||||
return toHttpBody(restClient.put().uri(buildUri(request, false))
|
||||
.contentType(contentType(request)).headers(headers -> applyHeaders(headers, request.getHeader()))
|
||||
.body(request.getBody()).retrieve().toEntity(String.class));
|
||||
}
|
||||
|
||||
private ResponseEntity<HttpBody> delete(HttpTransfer<?> request) {
|
||||
return toHttpBody(restClient.delete().uri(buildUri(request, true))
|
||||
.headers(headers -> applyHeaders(headers, request.getHeader()))
|
||||
.retrieve().toEntity(String.class));
|
||||
}
|
||||
|
||||
private String buildUri(HttpTransfer<?> request, boolean includeQueryParameters) {
|
||||
String uri = request.getDomain() + (request.getUri() == null ? "" : request.getUri());
|
||||
if (!includeQueryParameters || !(request.getBody() instanceof Map<?, ?> parameters)) {
|
||||
return uri;
|
||||
}
|
||||
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uri);
|
||||
parameters.forEach((key, value) -> { if (key != null && value != null) builder.queryParam(String.valueOf(key), value); });
|
||||
return builder.build().encode().toUriString();
|
||||
}
|
||||
|
||||
private MediaType contentType(HttpTransfer<?> request) {
|
||||
return request.getContentType() == null ? MediaType.APPLICATION_JSON : request.getContentType();
|
||||
}
|
||||
|
||||
private void applyHeaders(HttpHeaders target, HttpHeader source) {
|
||||
target.setAccept(List.of(MediaType.APPLICATION_JSON));
|
||||
source.getValues().forEach((name, value) -> { if (StringUtils.hasText(name) && StringUtils.hasText(value)) target.set(name, value); });
|
||||
}
|
||||
|
||||
private ResponseEntity<HttpBody> toHttpBody(ResponseEntity<String> response) {
|
||||
return new ResponseEntity<>(new HttpBody(response.getBody()), response.getHeaders(), response.getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package io.shinhanlife.glow.communication.module.http.dto;
|
||||
|
||||
/** Raw body returned by the Glow HTTP transport. */
|
||||
public record HttpBody(String content) {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.shinhanlife.glow.communication.module.http.dto;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/** Glow HTTP request headers and timeout metadata. */
|
||||
@Getter
|
||||
@Setter
|
||||
public class HttpHeader {
|
||||
private Map<String, String> values = new LinkedHashMap<>();
|
||||
private int readTimeout;
|
||||
|
||||
public void set(String name, String value) {
|
||||
values.put(name, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.shinhanlife.glow.communication.module.http.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
/** Glow HTTP request envelope. Use HttpTransfer.http() to build a request. */
|
||||
@Getter
|
||||
@Builder(builderMethodName = "http")
|
||||
public class HttpTransfer<T> {
|
||||
private final HttpHeader header;
|
||||
private final String domain;
|
||||
private final String uri;
|
||||
private final HttpMethod method;
|
||||
private final MediaType contentType;
|
||||
private final Class<?> responseEntity;
|
||||
private final T body;
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
# Glow 개발 환경 확장 설정
|
||||
# Glow 개발 환경 설정
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
on-profile: dev
|
||||
|
||||
glow:
|
||||
communication:
|
||||
common:
|
||||
env-type: D
|
||||
mci:
|
||||
host: ${GLOW_COMMUNICATION_MCI_HOST:https://dev-ichmci.shinhanlife.co.kr}
|
||||
port: ${GLOW_COMMUNICATION_MCI_PORT:26160}
|
||||
extmci:
|
||||
host: ${GLOW_COMMUNICATION_EXTMCI_HOST:http://host.docker.internal}
|
||||
port: ${GLOW_COMMUNICATION_EXTMCI_PORT:8080}
|
||||
eai:
|
||||
host: ${GLOW_COMMUNICATION_EAI_HOST:tcp://host.docker.internal}
|
||||
port: ${GLOW_COMMUNICATION_EAI_PORT:9999}
|
||||
@@ -1,5 +1,19 @@
|
||||
# Glow 로컬 환경 확장 설정
|
||||
# Glow 로컬 환경 설정
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: local
|
||||
on-profile: local
|
||||
|
||||
glow:
|
||||
communication:
|
||||
common:
|
||||
env-type: D
|
||||
mci:
|
||||
host: ${GLOW_COMMUNICATION_MCI_HOST:http://localhost}
|
||||
port: ${GLOW_COMMUNICATION_MCI_PORT:8080}
|
||||
extmci:
|
||||
host: ${GLOW_COMMUNICATION_EXTMCI_HOST:http://localhost}
|
||||
port: ${GLOW_COMMUNICATION_EXTMCI_PORT:8080}
|
||||
eai:
|
||||
host: ${GLOW_COMMUNICATION_EAI_HOST:http://localhost}
|
||||
port: ${GLOW_COMMUNICATION_EAI_PORT:8080}
|
||||
@@ -1,5 +1,19 @@
|
||||
# Glow 운영 환경 확장 설정
|
||||
# Glow 운영 환경 설정
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
on-profile: prod
|
||||
|
||||
glow:
|
||||
communication:
|
||||
common:
|
||||
env-type: P
|
||||
mci:
|
||||
host: ${GLOW_COMMUNICATION_MCI_HOST}
|
||||
port: ${GLOW_COMMUNICATION_MCI_PORT}
|
||||
extmci:
|
||||
host: ${GLOW_COMMUNICATION_EXTMCI_HOST}
|
||||
port: ${GLOW_COMMUNICATION_EXTMCI_PORT}
|
||||
eai:
|
||||
host: ${GLOW_COMMUNICATION_EAI_HOST}
|
||||
port: ${GLOW_COMMUNICATION_EAI_PORT}
|
||||
@@ -1,5 +1,19 @@
|
||||
# Glow 테스트 환경 확장 설정
|
||||
# Glow 테스트 환경 설정
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: test
|
||||
on-profile: test
|
||||
|
||||
glow:
|
||||
communication:
|
||||
common:
|
||||
env-type: T
|
||||
mci:
|
||||
host: ${GLOW_COMMUNICATION_MCI_HOST}
|
||||
port: ${GLOW_COMMUNICATION_MCI_PORT}
|
||||
extmci:
|
||||
host: ${GLOW_COMMUNICATION_EXTMCI_HOST}
|
||||
port: ${GLOW_COMMUNICATION_EXTMCI_PORT}
|
||||
eai:
|
||||
host: ${GLOW_COMMUNICATION_EAI_HOST}
|
||||
port: ${GLOW_COMMUNICATION_EAI_PORT}
|
||||
@@ -1,9 +1,5 @@
|
||||
# ==============================================================================
|
||||
# [AXHub 공통 환경 설정 파일] (Glow Framework 연동용)
|
||||
# 이 파일은 axhub-tool-core에 위치하며, 각 툴 파드들이 상속받아 사용합니다.
|
||||
# ==============================================================================
|
||||
|
||||
# 1. 로깅(Logging) 공통 설정
|
||||
# AX HUB Tool Pod 공통 Glow Framework 설정입니다.
|
||||
# Tool Pod의 application-{profile}.yml에서 이 파일과 환경별 Glow 설정을 함께 import합니다.
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
@@ -13,42 +9,38 @@ logging:
|
||||
pattern:
|
||||
console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n"
|
||||
|
||||
# 2. Redis 공통 설정 (Gateway 연동 및 캐싱)
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
host: 127.0.0.1 # TODO: 실제 Redis 망 IP로 변경
|
||||
port: 6379 # TODO: 실제 Redis 포트로 변경
|
||||
password: "" # TODO: 실제 비밀번호 입력
|
||||
host: ${SPRING_DATA_REDIS_HOST:127.0.0.1}
|
||||
port: ${SPRING_DATA_REDIS_PORT:6379}
|
||||
password: ${SPRING_DATA_REDIS_PASSWORD:}
|
||||
|
||||
# 3. 신한라이프 Glow Framework 통신 (MCI / EAI) 설정
|
||||
glow:
|
||||
communication: #Communication 설정
|
||||
common:
|
||||
env-type: D # 대내표준 헤더의 환경 타입정보 (D: 개발, T: 테스트, P: 운영)
|
||||
http: # HTTP 비표준 통신 설정
|
||||
connection-timeout: 5 # 연결 타임아웃 시간 (초 단위)
|
||||
read-timeout: 5 # 읽기 타임아웃 시간 (초 단위)
|
||||
mci: # 개발계 대내 MCI 통신 설정
|
||||
host: https://dev-ichmci.shinhanlife.co.kr
|
||||
port: 26160
|
||||
communication:
|
||||
http:
|
||||
connection-timeout: 5
|
||||
read-timeout: 5
|
||||
# HTTP Tool target catalog. Replace or add entries after the business endpoint is agreed.
|
||||
api-list:
|
||||
- name: sample
|
||||
domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8099}
|
||||
path: ""
|
||||
method: GET
|
||||
biz-pod: false
|
||||
mci:
|
||||
uri: /ntl_mci/dap_rcv
|
||||
receive-uri: /itrf/mciReceive
|
||||
connection-timeout: 300
|
||||
read-timeout: 300
|
||||
encoding: UTF-8
|
||||
extmci: # 대외MCI 통신 설정
|
||||
host: http://host.docker.internal # 대외MCI 호스트 주소
|
||||
port: 8080 # 대외MCI 포트 번호
|
||||
uri: /extmci # 대외MCI URI 정보
|
||||
json-uri: /extmciJson # 대외MCI JSON방식 URI 정보
|
||||
receive-uri: /app/extMciReceive # 대외MCI 수신 URI
|
||||
connection-timeout: 5 # 연결 타임아웃 시간 (초 단위)
|
||||
read-timeout: 30 # 읽기 타임아웃 시간 (초 단위)
|
||||
encoding: EUC-KR # 대외MCI 인코딩 정보
|
||||
eai: # EAI 통신 설정
|
||||
host: tcp://host.docker.internal # EAI 호스트 주소 (TODO: 실제 주소로 변경)
|
||||
port: 9999 # EAI 포트 번호
|
||||
extmci:
|
||||
uri: /extmci
|
||||
json-uri: /extmciJson
|
||||
receive-uri: /app/extMciReceive
|
||||
connection-timeout: 5
|
||||
read-timeout: 30
|
||||
encoding: EUC-KR
|
||||
websocket:
|
||||
endpoint: "/ws-glow"
|
||||
allowed-origins: "*"
|
||||
endpoint: /ws-glow
|
||||
allowed-origins: "*"
|
||||
Reference in New Issue
Block a user