diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java index ef21f7070..2ddbc1c25 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java @@ -58,7 +58,7 @@ public class EaiEimsSender implements EimsSender { log.warn(" 로컬 환경이거나 Ka\n" + "\n" + "
\n" + - " MCP_WAS_ASYNC\n" + + " MCP_GATEWAY_ASYNC\n" + " EAI_BATCH_JOB\n" + " 1782705901850\n" + " ASYNC\n" + @@ -91,7 +91,7 @@ public class EaiEimsSender implements EimsSender { return String.format( "" + "
" + - "MCP_WAS_ASYNC" + + "MCP_GATEWAY_ASYNC" + "%s" + "%d" + "ASYNC" + diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java deleted file mode 100644 index aa0d6aeba..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpFunction - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import java.lang.annotation.*; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpFunction { - String displayName(); // 사람이 읽는 라벨 (예: "고객 조회 툴") - String name(); // MCP 서브툴 명칭 (예: "customer_search") - String description(); - String prompt() default ""; - String mappingId() default ""; - - - /** - * Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다. - */ - String inputSchema() default "{}"; - - /** - * 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다. - * inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다. - */ - // 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어 - String inputSchemaResource() default ""; - - /** - * Tool response JSON Schema. When unset, output validation is skipped. - */ - String outputSchema() default "{}"; - - /** - * Classpath resource for a complex Tool response JSON Schema. - * This value has priority over outputSchema. - */ - String outputSchemaResource() default ""; - boolean register() default false; - - // 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김) - boolean visible() default true; - - // 추가: HITL 승인 체계 지원 (실행 전 사용자 승인 필요 여부) - boolean requiresApproval() default false; - - boolean readOnlyHint() default false; - boolean destructiveHint() default false; - boolean idempotentHint() default false; - boolean openWorldHint() default false; - - /** Version exposed as _meta.version in the Tool Manifest. */ - String version() default "1.0.0"; - - /** Maximum execution time exposed as _meta.timeoutMillis in the Tool Manifest. */ - long timeoutMillis() default 300000L; - - /** Whether the Tool is available for MCP exposure. */ - boolean enabled() default true; - - // 추가: 툴 별 기본 Timeout 설정 (기본 300초 = 300000ms) -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java deleted file mode 100644 index 49270784c..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpOutputSchema - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a Tool response DTO for automatic output JSON Schema generation. - * Field constraints are declared with {@link McpValidation}. - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpOutputSchema { -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java deleted file mode 100644 index 0497a02a3..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpParameter - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import java.lang.annotation.*; - -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpParameter { - String description(); - boolean required() default false; -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java deleted file mode 100644 index 340177865..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpTool - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import org.springframework.core.annotation.AliasFor; -import org.springframework.stereotype.Component; - -import java.lang.annotation.*; - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Component -public @interface McpTool { - @AliasFor(annotation = Component.class) - String value() default ""; - - String categoryKey() default "common"; - String routingType() default "HTTP"; -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java deleted file mode 100644 index 0def3e577..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpValidation - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpValidation - * @description Declares JSON Schema validation constraints for MCP tool input fields - * @author 0986406 - * @create 2026.07.27 - *
- * ---------- revision history ----------
- * date       author    description
- * ---------- --------- ---------------------------
- * 2026.07.27 0986406    initial creation
- * 
- */ -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpValidation { - boolean required() default false; - String pattern() default ""; - long minimum() default Long.MIN_VALUE; - long maximum() default Long.MAX_VALUE; - int minLength() default -1; - int maxLength() default -1; - String[] allowedValues() default {}; - String format() default ""; - boolean nullable() default false; String defaultValue() default ""; - String[] examples() default {}; -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java similarity index 51% rename from dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java rename to dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java index cdfc10617..1a42b32d7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java @@ -1,9 +1,15 @@ 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; + /** + * Spring AI @Tool 어노테이션을 보완하여 MCP 시스템 메타데이터를 추가 제공하는 힌트 어노테이션 * @package io.shinhanlife.dap.lib.annotation - * @className McpAnyOf - * @description AX HUB 시스템 처리 클래스 + * @className ToolHint + * @description 비즈니스 로직(Tool)과 시스템 제어 메타데이터 분리 * @author 0986406 * @create 2026.09.01 *
@@ -14,22 +20,12 @@ 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; - -/** - * MCP 스키마 생성 시 anyOf (해당 필드들 중 최소 1개 이상 필수) 제약을 부여합니다. - */ -@Target({ElementType.TYPE}) +@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) -public @interface McpAnyOf { - /** - * anyOf 제약에 포함될 필드명 목록 - * 예: @McpAnyOf({"claimNo", "contractNo"}) - */ - String[] value(); +public @interface ToolHint { + boolean register() default false; + boolean requiresApproval() default false; + String mappingId() default ""; + String inputSchemaResource() default ""; + String outputSchemaResource() default ""; } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java index a7aef5eb3..7408c57df 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java @@ -15,7 +15,6 @@ package io.shinhanlife.dap.lib.aop; * * */ -import io.shinhanlife.dap.lib.annotation.McpFunction; import io.shinhanlife.dap.lib.config.McpProperties; import java.lang.reflect.Method; import lombok.RequiredArgsConstructor; @@ -24,6 +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.springframework.stereotype.Component; import org.springframework.util.StopWatch; @@ -35,12 +35,12 @@ public class ToolSlaMonitoringAspect { private final McpProperties mcpProperties; - // @McpFunction 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다. - @Around("@annotation(McpFunction)") + // @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다. + @Around("@annotation(org.springframework.ai.mcp.annotation.McpTool)") public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); - McpFunction functionAnnotation = method.getAnnotation(McpFunction.class); + McpTool functionAnnotation = method.getAnnotation(McpTool.class); // 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다. String baseName = functionAnnotation.name(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java index d507f890e..0364a5e59 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.config; -/** - * @package io.shinhanlife.dap.lib.config - * @className ToolSchemaConfiguration - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import org.springframework.context.annotation.Bean; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java new file mode 100644 index 000000000..6d8684007 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java @@ -0,0 +1,20 @@ +package io.shinhanlife.dap.lib.dto; + +/** + * @package io.shinhanlife.dap.mcg.dto + * @className OperationType + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public enum OperationType { + READ, + WRITE +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java index 6ad8defa5..f793904ec 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestAnnotations - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Behaviour hints exposed by the Tool Service manifest. */ public record ToolManifestAnnotations( String title, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java index dece89006..f03c9a96d 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestItem - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Map; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java index 2021c8bc7..192ba496a 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestMeta - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Operational metadata exposed by the Tool Service manifest. */ public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) { } \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java index 8284c0be3..293a9e01b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestResponse - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.util.List; /** Top-level response for GET /tool-manifest. */ diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java index 44f0eeef8..5050a442a 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java @@ -1,25 +1,9 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestService - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.mcp.LocalToolScanner; +import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Comparator; @@ -46,9 +30,9 @@ public class ToolManifestService { private String lastRevision; @Autowired - public ToolManifestService(LocalToolScanner localToolScanner, ObjectMapper objectMapper, + public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper, McpProperties properties) { - this(localToolScanner::getAllScannedTools, objectMapper, properties); + this(heartbeatSender::getAllScannedTools, objectMapper, properties); } ToolManifestService(Supplier> toolSupplier, ObjectMapper objectMapper, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java deleted file mode 100644 index 183bd61c7..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.shinhanlife.dap.lib.mcp; - - -/** - * @package io.shinhanlife.dap.mcc.service - * @className LocalToolScanner - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import com.fasterxml.jackson.databind.ObjectMapper; -import io.shinhanlife.dap.lib.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpTool; -import io.shinhanlife.dap.lib.config.McpProperties; -import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.util.ToolSchemaResolver; -import jakarta.annotation.PostConstruct; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.stereotype.Component; -import org.springframework.util.ClassUtils; - -@Slf4j -@Component -@Configuration -@RequiredArgsConstructor -public class LocalToolScanner { - - private final ApplicationContext applicationContext; - private final ObjectMapper objectMapper; - private final McpProperties mcpProperties; - private final ToolSchemaResolver toolSchemaResolver; - - @Value("${axhub.tool.url:http://localhost:8080}") - private String podUrl; - - private List registeredTools = new ArrayList<>(); - - @Getter - private List allScannedTools = new ArrayList<>(); - - @PostConstruct - public void init() { - log.info(" [LocalToolScanner] 초기화 시작. Pod URL: {}", podUrl); - scanAndBuildMetadata(); - } - - private void scanAndBuildMetadata() { - Map allBeans = applicationContext.getBeansOfType(Object.class); - for (Object bean : allBeans.values()) { - Class targetClass = AopUtils.getTargetClass(bean); - - // 클래스 또는 프록시(인터페이스)에서 @McpTool 스캔 - McpTool toolAnnotation = AnnotationUtils.findAnnotation(targetClass, McpTool.class); - if (toolAnnotation == null) { - toolAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), McpTool.class); - } - - for (Method method : targetClass.getDeclaredMethods()) { - // 메서드, 수퍼클래스, 인터페이스를 모두 뒤져서 @McpFunction 스캔 - McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class); - - if (functionAnnotation != null && toolAnnotation != null) { - String baseName = functionAnnotation.displayName(); - String rawSubToolName = functionAnnotation.name(); - String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() - ? mcpProperties.getNamespace() + "_" + rawSubToolName - : rawSubToolName; - - boolean isRegister = functionAnnotation.register(); - if (!isRegister) { - log.info(" [LocalToolScanner] '{}' 툴은 어노테이션 설정에 의해 제외되었습니다. (최종 이름: {})", baseName, subToolName); - } - - ToolMetadata meta = new ToolMetadata(); - meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString()); - meta.setDisplayName(baseName); - meta.setName(subToolName); - meta.setSemver(functionAnnotation.version()); - meta.setTimeoutMillis(functionAnnotation.timeoutMillis()); - meta.setEnabled(functionAnnotation.enabled()); - meta.setDescription(functionAnnotation.description()); - meta.setCategoryKey(toolAnnotation.categoryKey()); - meta.setIntegrationType(toolAnnotation.routingType()); - meta.setMciServiceId(functionAnnotation.mappingId()); - meta.setPodUrl(podUrl); - meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName); - - boolean isVisible = functionAnnotation.visible(); - meta.setVisible(isVisible); - meta.setIsRegistered(isRegister); - meta.setRequiresApproval(functionAnnotation.requiresApproval()); - meta.setReadOnlyHint(functionAnnotation.readOnlyHint()); - meta.setDestructiveHint(functionAnnotation.destructiveHint()); - meta.setIdempotentHint(functionAnnotation.idempotentHint()); - meta.setOpenWorldHint(functionAnnotation.openWorldHint()); - - Map prompts = new HashMap<>(); - String promptText = functionAnnotation.prompt(); - prompts.put(subToolName, promptText); - meta.setActionPrompts(prompts); - - if (method.getParameterCount() > 0) { - try { - Class paramType = method.getParameterTypes()[0]; - Map finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType); - meta.setParametersSchema(finalSchema); - } catch (Exception e) { - log.error("Failed to generate schema for {}", subToolName, e); - } - } - - if (isRegister) { - registeredTools.add(meta); - } - allScannedTools.add(meta); - log.info(" [LocalToolScanner] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister); - } - } - } - } - -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java index a96608926..0104394df 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className McpRequestHeaderContext - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Holds optional MCP headers for the lifetime of one HTTP request thread. */ public final class McpRequestHeaderContext { private static final ThreadLocal CURRENT_HEADERS = new ThreadLocal<>(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java index b90a67c35..aae2654f4 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className McpRequestHeaderFilter - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.io.IOException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java index b88a6698c..3eb87c766 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className McpRequestHeaders - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */ public record McpRequestHeaders( String headerRequestId, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java index 50713588d..90afd71bd 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java @@ -1,31 +1,25 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className ToolMcpServerConfiguration - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; /** Exposes every Tool Pod through the MCP Streamable HTTP transport. */ +@Slf4j @Configuration public class ToolMcpServerConfiguration { + @PostConstruct + public void init() { + log.warn("================================================="); + log.warn("ToolMcpServerConfiguration IS LOADED BY SPRING!!!"); + log.warn("================================================="); + } + @Bean @Primary public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() { @@ -37,6 +31,8 @@ public class ToolMcpServerConfiguration { @Bean public ServletRegistrationBean toolMcpServlet( HttpServletStreamableServerTransportProvider transportProvider) { - return new ServletRegistrationBean<>(transportProvider, "/mcp"); + // "/mcp/*"로 매핑하면 BusinessToolController의 "/mcp/api/v1/tools/local" 까지 가로채게 되므로, + // 정확히 MCP 통신에 사용되는 "/mcp" 와 "/mcp/message" 두 개만 매핑합니다. + return new ServletRegistrationBean<>(transportProvider, "/mcp", "/mcp/message"); } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java index 3695e6af0..d9f23f019 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java @@ -1,28 +1,14 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className ToolPodMcpToolSynchronizer - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; 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.LocalToolScanner; +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.Map; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -30,25 +16,28 @@ 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) public class ToolPodMcpToolSynchronizer { private final McpSyncServer mcpServer; - private final LocalToolScanner localToolScanner; + private final ToolRegistryHeartbeatSender heartbeatSender; private final BusinessToolController businessToolController; private final ObjectMapper objectMapper; - public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, LocalToolScanner localToolScanner, + public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender, BusinessToolController businessToolController, ObjectMapper objectMapper) { this.mcpServer = mcpServer; - this.localToolScanner = localToolScanner; + this.heartbeatSender = heartbeatSender; this.businessToolController = businessToolController; this.objectMapper = objectMapper; } @EventListener(ApplicationReadyEvent.class) public void registerLocalTools() { - localToolScanner.getAllScannedTools().stream() + heartbeatSender.getAllScannedTools().stream() .filter(tool -> Boolean.TRUE.equals(tool.getVisible())) .forEach(tool -> mcpServer.addTool(specification(tool))); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java new file mode 100644 index 000000000..e80d90a46 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java @@ -0,0 +1,196 @@ +package io.shinhanlife.dap.lib.mcp; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className ToolRegistryHeartbeatSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.mcp.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import jakarta.annotation.PostConstruct; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +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) +public class ToolRegistryHeartbeatSender { + + private final ApplicationContext applicationContext; + private final ObjectMapper objectMapper; + private final McpProperties mcpProperties; + private final RestClient restClient = RestClient.create(); + private final ToolSchemaResolver toolSchemaResolver; + + @Value("${axhub.gateway.url:http://localhost:8081}") + private String gatewayUrl; + + @Value("${axhub.tool.url:http://localhost:8080}") + private String podUrl; + + private List registeredTools = new ArrayList<>(); + + @Getter + private List allScannedTools = new ArrayList<>(); + + @PostConstruct + public void init() { + log.info(" [HeartbeatSender] 초기화 시작. Gateway URL: {}, Pod URL: {}", gatewayUrl, podUrl); + scanAndBuildMetadata(); + } + + private void scanAndBuildMetadata() { + Map allBeans = applicationContext.getBeansOfType(Object.class); + for (Object bean : allBeans.values()) { + Class targetClass = AopUtils.getTargetClass(bean); + + for (Method method : targetClass.getDeclaredMethods()) { + McpTool functionAnnotation = AnnotationUtils.findAnnotation(method, McpTool.class); + ToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, ToolHint.class); + + if (functionAnnotation != null) { + String baseName = functionAnnotation.name(); + String rawSubToolName = functionAnnotation.name(); + String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() + ? mcpProperties.getNamespace() + "_" + rawSubToolName + : rawSubToolName; + + boolean isRegister = hintAnnotation != null && hintAnnotation.register(); + if (!isRegister) { + log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName); + } + + ToolMetadata meta = new ToolMetadata(); + meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString()); + String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title(); + meta.setDisplayName(displayName); + meta.setName(subToolName); + meta.setSemver("1.0.0"); + meta.setTimeoutMillis(5000L); + meta.setEnabled(true); + meta.setDescription(functionAnnotation.description()); + meta.setCategoryKey("default"); + meta.setIntegrationType("REST"); + meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : ""); + meta.setPodUrl(podUrl); + meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName); + + meta.setVisible(true); + meta.setIsRegistered(isRegister); + meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval()); + + // extract standard hints from @McpTool.annotations() + McpTool.McpAnnotations ann = functionAnnotation.annotations(); + if (ann != null) { + meta.setReadOnlyHint(ann.readOnlyHint()); + meta.setDestructiveHint(ann.destructiveHint()); + meta.setIdempotentHint(ann.idempotentHint()); + meta.setOpenWorldHint(ann.openWorldHint()); + } else { + meta.setReadOnlyHint(false); + meta.setDestructiveHint(true); + meta.setIdempotentHint(false); + meta.setOpenWorldHint(true); + } + + Map prompts = new HashMap<>(); + meta.setActionPrompts(prompts); + + if (method.getParameterCount() > 0) { + try { + Class paramType = method.getParameterTypes()[0]; + // TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction + Map finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType); + meta.setParametersSchema(finalSchema); + } catch (Exception e) { + log.error("Failed to generate schema for {}", subToolName, e); + } + } + + if (isRegister) { + registeredTools.add(meta); + } + allScannedTools.add(meta); + log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister); + } + } + } + } + + @Scheduled(fixedRate = 30000) + public void sendHeartbeats() { + if (registeredTools.isEmpty()) return; + + for (ToolMetadata tool : registeredTools) { + try { + ResponseEntity response = restClient.post() + .uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat") + .header("Content-Type", "application/json") + + .body(tool.getUid()) + .retrieve() + .toEntity(String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid()); + } + } catch (Exception e) { + log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage()); + registerTool(tool); + } + } + } + + private void registerTool(ToolMetadata tool) { + try { + restClient.post() + .uri(gatewayUrl + "/mcp/api/v1/registry/register") + .header("Content-Type", "application/json") + + .body(tool) + .retrieve() + .toBodilessEntity(); + log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid()); + } catch (Exception ex) { + log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage()); + } + } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java index 26f5f67d8..5c2e22c96 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java @@ -29,11 +29,11 @@ public class SwaggerConfig { public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() - .title("Shinhan MCP WAS API 명세서") + .title("Shinhan MCP Gateway API 명세서") .version("v1.0") - .description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter WAS API 문서입니다.")) + .description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다.")) .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)")) - .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("WAS Pod")) + .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)")) // 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다. .addSecurityItem(new SecurityRequirement().addList("X-API-KEY")) .components(new Components() diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java index 8bfbcc2fb..9e3cfc8b4 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java @@ -43,7 +43,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { - // Swagger UI(8080)에서 WAS로 API 호출 시 발생하는 CORS 에러 해결 + // Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결 registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java index 065d8012e..f5700193f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java @@ -29,25 +29,25 @@ public class GlobalExceptionHandler { @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity handleNoResourceFound(NoResourceFoundException e) { - log.warn(" [WAS Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath()); + log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath()); return ResponseEntity.notFound().build(); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { - log.warn(" [WAS Bad Request] 잘못된 요청: {}", e.getMessage()); + log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage()); return buildErrorResponse(-32602, "Invalid params: " + e.getMessage()); } @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntime(RuntimeException e) { - log.error(" [WAS Internal Error] 시스템 장애: {}", e.getMessage(), e); + log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e); return buildErrorResponse(-32603, "Internal error: " + e.getMessage()); } @ExceptionHandler(Exception.class) public ResponseEntity handleAllException(Exception e) { - log.error(" [WAS Fatal Error] 치명적 오류 발생", e); + log.error(" [Gateway Fatal Error] 치명적 오류 발생", e); return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요."); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java deleted file mode 100644 index a440f3957..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.shinhanlife.dap.lib.session.presentation; - -import io.micrometer.common.util.StringUtils; -import io.shinhanlife.glow.BaseResponse; -import io.shinhanlife.glow.BizException; -import io.shinhanlife.glow.GlowControllerId; -import io.shinhanlife.glow.ResponseUtil; -import io.shinhanlife.dap.lib.session.dto.SessionDto; -import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.javassist.NotFoundException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * @package io.shinhanlife.dap.lib.session.presentation - * @className SsoRestController - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -@RestController -@RequiredArgsConstructor -@Slf4j -@RequestMapping("/sso") -public class SsoRestController { - - private static final String NLS_LOGIN_URL = ""; - - /** - * sso 연동 전 임시 로그인 - * - * @param request - * @param response - * @param session - * @param - * @return - */ - @GlowControllerId("tempLogin") - @PostMapping("/tempLogin") - public ResponseEntity> tempLogin(HttpServletRequest request, HttpServletResponse response, - HttpSession session, @RequestBody SessionDto requestDto) { - try { - if (StringUtils.isEmpty(requestDto.getPrafNo())) { - throw new NotFoundException("SSO >> not found sso id"); - } - - // DB 조회(ZtUsac) 없이 파라미터로 받은 SessionDto를 그대로 사용 - SessionDto sessionDto = requestDto; - sessionDto.setLoginDtm(); // 로그인 시점 세팅 - session.setAttribute("userInfo", sessionDto); - return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build()); - - } catch (Exception e) { - log.error("로그인 실패", e); - session.invalidate(); - } - - return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build()); - } -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java deleted file mode 100644 index 10594ebd4..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.shinhanlife.dap.lib.session.presentation.io; - -import io.shinhanlife.dap.lib.session.dto.SessionDto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -/** - * @package io.shinhanlife.dap.lib.session.presentation.io - * @className SsoResponse - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SsoResponse { - - private String retCode; - private SessionDto userInfo; - private String redirectUrl; - -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java index d95dae6e3..1e4938049 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java @@ -2,9 +2,8 @@ package io.shinhanlife.dap.lib.util; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import io.shinhanlife.dap.lib.annotation.McpParameter; -import io.shinhanlife.dap.lib.annotation.McpValidation; -import io.shinhanlife.dap.lib.annotation.McpAnyOf; +import org.springframework.ai.mcp.annotation.McpToolParam; +import io.swagger.v3.oas.annotations.media.Schema; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -56,7 +55,7 @@ public class JsonSchemaGenerator { // 1. 타입 매핑 // 2. 어노테이션 기반 설명 추출 - McpParameter paramAnnotation = field.getAnnotation(McpParameter.class); + McpToolParam paramAnnotation = field.getAnnotation(McpToolParam.class); JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class); if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) { fieldSchema.put("description", paramAnnotation.description()); @@ -72,46 +71,54 @@ public class JsonSchemaGenerator { requiredList.add(field.getName()); } - McpValidation validation = field.getAnnotation(McpValidation.class); - if (validation != null && validation.required() && !requiredList.contains(field.getName())) { - requiredList.add(field.getName()); + Schema schemaAnnotation = field.getAnnotation(Schema.class); + if (schemaAnnotation != null) { + if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) { + fieldSchema.put("description", schemaAnnotation.description()); + } + if (schemaAnnotation.required() && !requiredList.contains(field.getName())) { + requiredList.add(field.getName()); + } + if (!schemaAnnotation.pattern().isEmpty()) { + fieldSchema.put("pattern", schemaAnnotation.pattern()); + } + if (!schemaAnnotation.minimum().isEmpty()) { + try { + fieldSchema.put("minimum", Long.valueOf(schemaAnnotation.minimum())); + } catch (NumberFormatException ignored) {} + } + if (!schemaAnnotation.maximum().isEmpty()) { + try { + fieldSchema.put("maximum", Long.valueOf(schemaAnnotation.maximum())); + } catch (NumberFormatException ignored) {} + } + if (schemaAnnotation.minLength() > 0) { + fieldSchema.put("minLength", schemaAnnotation.minLength()); + } + if (schemaAnnotation.maxLength() > 0 && schemaAnnotation.maxLength() != Integer.MAX_VALUE) { + fieldSchema.put("maxLength", schemaAnnotation.maxLength()); + } + if (schemaAnnotation.allowableValues().length > 0 && !schemaAnnotation.allowableValues()[0].isEmpty()) { + fieldSchema.put("enum", List.of(schemaAnnotation.allowableValues())); + } + if (!schemaAnnotation.format().isEmpty()) { + fieldSchema.put("format", schemaAnnotation.format()); + } + if (!schemaAnnotation.defaultValue().isEmpty()) { + fieldSchema.put("default", coerceDefaultValue(schemaAnnotation.defaultValue(), field.getType())); + } + if (!schemaAnnotation.example().isEmpty()) { + fieldSchema.put("examples", List.of(schemaAnnotation.example())); + } + if (schemaAnnotation.nullable()) { + Map nonNullSchema = new HashMap<>(fieldSchema); + fieldSchema = new HashMap<>(); + fieldSchema.put("anyOf", List.of( + nonNullSchema, + Map.of("type", "null") + )); + } } - if (validation != null && !validation.pattern().isEmpty()) { - fieldSchema.put("pattern", validation.pattern()); - } - if (validation != null && validation.minimum() != Long.MIN_VALUE) { - fieldSchema.put("minimum", validation.minimum()); - } - if (validation != null && validation.maximum() != Long.MAX_VALUE) { - fieldSchema.put("maximum", validation.maximum()); - } - if (validation != null && validation.minLength() >= 0) { - fieldSchema.put("minLength", validation.minLength()); - } - if (validation != null && validation.maxLength() >= 0) { - fieldSchema.put("maxLength", validation.maxLength()); - } - if (validation != null && validation.allowedValues().length > 0) { - fieldSchema.put("enum", List.of(validation.allowedValues())); - } - if (validation != null && !validation.format().isEmpty()) { - fieldSchema.put("format", validation.format()); - } - if (validation != null && !validation.defaultValue().isEmpty()) { - fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType())); - } - if (validation != null && validation.examples().length > 0) { - fieldSchema.put("examples", List.of(validation.examples())); - } - if (validation != null && validation.nullable()) { - Map nonNullSchema = new HashMap<>(fieldSchema); - fieldSchema = new HashMap<>(); - fieldSchema.put("anyOf", List.of( - nonNullSchema, - Map.of("type", "null") - )); - } - properties.put(field.getName(), fieldSchema); } @@ -120,15 +127,7 @@ public class JsonSchemaGenerator { schema.put("required", requiredList); } - McpAnyOf anyOfAnnotation = clazz.getAnnotation(McpAnyOf.class); - if (anyOfAnnotation != null && anyOfAnnotation.value().length > 0) { - List> anyOfList = new ArrayList<>(); - for (String fieldName : anyOfAnnotation.value()) { - anyOfList.add(Map.of("required", List.of(fieldName))); - - } - schema.put("anyOf", anyOfList); - } + // anyOf removed visiting.remove(clazz); return schema; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java new file mode 100644 index 000000000..ea648ea72 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java @@ -0,0 +1,309 @@ +package io.shinhanlife.dap.lib.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Scanner; + +public class PodScaffolder { + + public static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================="); + System.out.println(" MCP Tool Pod Scaffolder (Java CLI) "); + System.out.println("=========================================\n"); + + String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-was-payment): "); + String moduleName = rawModuleName.startsWith("dap-was-") ? rawModuleName : "dap-was-" + rawModuleName; + String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): "); + String shortName = moduleName.replace("dap-was-", "").replace("-", ""); + + String defaultAuthor = System.getProperty("user.name"); + String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + if (author.trim().isEmpty()) author = defaultAuthor; + String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + if (createDate.trim().isEmpty()) createDate = defaultDate; + + String result = scaffoldPod(moduleName, portStr, shortName, author, createDate); + System.out.println(result); + } + + private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) { + if (args.length > index) { + return args[index]; + } + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + 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("."); + + Path modulePath = rootDir.resolve(Paths.get(moduleName)); + if (Files.exists(modulePath)) { + return "[오류] 이미 존재하는 모듈입니다: " + moduleName; + } + + StringBuilder log = new StringBuilder(); + log.append("[1/6] 모듈 디렉터리 생성 중...\n"); + Files.createDirectories(modulePath); + + log.append("[2/6] build.gradle 생성 중...\n"); + String buildGradle = """ + plugins { + id 'org.springframework.boot' + } + dependencies { + 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 + WORKDIR /app + COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar + ENTRYPOINT ["java", "-jar", "app.jar"] + """.formatted(moduleName); + Files.writeString(modulePath.resolve("Dockerfile"), dockerfile); + + log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n"); + Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName); + Files.createDirectories(srcPath); + + String appClass = """ + package io.shinhanlife.dap.mcc.%s; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + import org.springframework.cache.annotation.EnableCaching; + + /** + * @package io.shinhanlife.dap.mcc.%s + * @className DapWas%sApplication + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"}) + @ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"}) + @EnableCaching + public class DapWas%sApplication { + public static void main(String[] args) { + SpringApplication.run(DapWas%sApplication.class, args); + } + } + """.formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName)); + Files.writeString(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass); + + Path resPath = modulePath.resolve("src/main/resources"); + Files.createDirectories(resPath); + String applicationYml = """ + server: + port: %s + spring: + application: + name: %s + profiles: + active: local + logging: + level: + org.apache.kafka: ERROR + mcp: + namespace: "" + security: + tenant-domains: + TESTER-DEV: ALL + """.formatted(portStr, moduleName); + Files.writeString(resPath.resolve("application.yml"), applicationYml); + + String applicationLocalYml = """ + # Local 환경 전용 설정 (H2 메모리 DB 등) + spring: + datasource: + url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; + driverClassName: com.p6spy.engine.spy.P6SpyDriver + username: sa + password: password + h2: + console: + enabled: true + + eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + + mcp: + security: + tenant-domains: + mcp-client-1: CUSTOMER,COMMON + mcp-client-2: ALL + + axhub: + gateway: + url: http://localhost:8081 + tool: + url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} + """; + Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml); + + String applicationDevYml = """ + # OCI 클라우드 환경 전용 설정 + server: + port: ${PORT:%s} + + axhub: + gateway: + url: https://axhubmcp.devjun.net + tool: + url: http://144.24.70.100:%s + + eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + + shinhan: + integration: + envrTypeCd: D + eai: + url: http://10.176.32.181 + internalMci: + url: http://10.176.32.173 + bancaMci: + url: http://10.176.32.117 + externalMci: + url: http://10.176.32.176 + """.formatted(portStr, portStr); + Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml); + + String logbackXml = """ + + + + + + ${LOG_PATTERN} + + + + logs/%s.log + + logs/%s-%%d{yyyy-MM-dd}.log + 30 + + + ${LOG_PATTERN} + + + + + + + + + """.formatted(moduleName, moduleName); + Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml); + + log.append("[5/6] settings.gradle 에 모듈 등록 중...\n"); + Path settingsPath = rootDir.resolve(Paths.get("settings.gradle")); + if (Files.exists(settingsPath)) { + String settings = Files.readString(settingsPath); + if (!settings.contains("include '" + moduleName + "'")) { + Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND); + } + } + + log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n"); + Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml")); + if (Files.exists(dockerComposePath)) { + String compose = Files.readString(dockerComposePath); + String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment + if (!compose.contains(" " + serviceName + ":")) { + String newService = """ + %s: + build: + context: . + dockerfile: %s/Dockerfile + ports: + - "%s:%s" + depends_on: + - redis + environment: + - TZ=Asia/Seoul + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - SPRING_DATA_REDIS_PORT=6379 + - AXHUB_GATEWAY_URL=http://gateway:8081 + - AXHUB_TOOL_URL=http://%s:%s + - GLOW_COMMUNICATION_MCI_HOST=http://mci-mock + - GLOW_COMMUNICATION_MCI_PORT=8080 + - GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock + - GLOW_COMMUNICATION_EXTMCI_PORT=8080 + - GLOW_COMMUNICATION_EAI_HOST=http://mci-mock + - GLOW_COMMUNICATION_EAI_PORT=8080 + """.formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr); + Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND); + } + } + + log.append("\n=========================================\n"); + log.append(" Pod Scaffolding Complete! \n"); + log.append("=========================================\n"); + log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n"); + log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n"); + log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n"); + return log.toString(); + } + + private static String capitalize(String str) { + if (str == null || str.isEmpty()) return str; + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java new file mode 100644 index 000000000..3f4ed4fbe --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -0,0 +1,783 @@ +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.Locale; +import java.util.Scanner; + +/** + * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스 + * + * [실행 방법] + * 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐) + * - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다. + * - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다. + * + * 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반) + * - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java + * - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]" + */ +/** + * @package io.shinhanlife.dap.lib.util + * @className ToolScaffolder + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +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 static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================="); + System.out.println(" MCP Tool Scaffolder (Java CLI) "); + System.out.println("=========================================\n"); + + String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: "); + String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): "); + String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): "); + String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): "); + if (group.isEmpty()) group = "COMMON"; + String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): "); + if (routingType.trim().isEmpty()) { + routingType = "HTTP"; + } + String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-was-oth): "); + if (moduleName.trim().isEmpty()) { + moduleName = "dap-was-oth"; + } + + String defaultAuthor = System.getProperty("user.name"); + String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + if (author.trim().isEmpty()) author = defaultAuthor; + String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + if (createDate.trim().isEmpty()) createDate = defaultDate; + + String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): "); + boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim()); + + String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, useSchemaResource); + System.out.println(result); + } + + private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) { + if (args.length > index) { + return args[index]; + } + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException { + return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, register, clientSystemCode, false); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, boolean useSchemaResource) throws IOException { + baseName = toPascalCase(baseName); + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); + Path usecaseImplDir = usecaseDir.resolve("impl"); + Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto")); + + Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy")); + Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter")); + + // schema resource 파일 경로 (useSchemaResource=true 일 때만 생성) + 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; + + 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); + mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath)); + } + + Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io")); + + Files.createDirectories(usecaseDir); + Files.createDirectories(usecaseImplDir); + Files.createDirectories(dtoDir); + if (isMci) { + Files.createDirectories(mciIoDir); + if (mciClientDir != null) { + Files.createDirectories(mciClientDir); + } + } else { + Files.createDirectories(legacyDtoDir); + } + Files.createDirectories(converterDir); + + StringBuilder log = new StringBuilder(); + + // Generate Request DTO + String reqContent = """ + package %s.dto; + + import com.fasterxml.jackson.annotation.JsonInclude; + import lombok.Data; + + /** + * @package %s.dto + * @className %sRequest + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %sRequest { + @McpToolParam(description = "수신자 전화번호", required = true) + -(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"}) + private String phoneNumber; + + @McpToolParam(description = "전송할 메시지 내용", required = true) + private String message; + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); + + // Generate Response DTO + String resContent = """ + package %s.dto; + + import com.fasterxml.jackson.annotation.JsonInclude; + import lombok.Data; + + /** + * @package %s.dto + * @className %sResponse + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %sResponse { + private String resultCode; + + private String resultMessage; + + // TODO: Add response fields here. Do not include PII in the Tool response. + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + 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); + } else { + toolHintLine = " @ToolHint(register = %s)".formatted(register); + } + + String serviceInterfaceContent = """ + package %s.usecase; + + import org.springframework.ai.mcp.annotation.McpTool; + import io.shinhanlife.dap.lib.annotation.ToolHint; + import %s.dto.%sRequest; + import %s.dto.%sResponse; + + /** + * @package %s.usecase + * @className %sUseCase + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             *
+             * 
+ */ + public interface %sUseCase { + + @McpTool(name = "%s", title = "%s", description = "%s") + %s + %sResponse execute(%sRequest req); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, author, createDate, createDate, author, + baseName, + toolName, description, description, + toolHintLine, + baseName, baseName + ); + + Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent); + + String serviceImplContent; + + if (isMci) { + serviceImplContent = """ + package %s.usecase.impl; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + 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 + + /** + * @package %s.usecase.impl + * @className %sUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl implements %sUseCase { + + %s + private final %sConverter converter; + + @Override + public Object execute(%sRequest req) { + log.info("[MCI Tool] {} 요청 수신.", "%s"); + try { + // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) + %s_I mciReq = converter.toLegacyRequest(req); + + Transfer resTransfer = mci.callTo( + "%s", + null, + mciReq, + Object.class + ); + return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS"); + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + (clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"), + bizPackage, + baseName, + author, + createDate, + createDate, author, + baseName, + baseName, + (clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"), + baseName, + baseName, + toolName, + interfaceId, + interfaceId + ); + } else { + serviceImplContent = """ + package %s.usecase.impl; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; + import %s.converter.%sConverter; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + + /** + * @package %s.usecase.impl + * @className %sUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase { + + private final %sConverter converter; + + @Override + public Object execute(%sRequest req) { + // %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); + return executeLegacy("%s", "%s", req); // Or pass legacyRequest + } + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + author, + createDate, + createDate, author, + baseName, baseName, + baseName, + baseName, + baseName, + routingType, interfaceId + ); + } + + Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent); + + if (isMci) { + String mciReqContent = """ + package %s.%s.io; + + import lombok.Data; + + /** + * @package %s.%s.io + * @className %s_I + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %s_I { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; + } + """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent); + + String mciResContent = """ + package %s.%s.io; + + import lombok.Data; + + /** + * @package %s.%s.io + * @className %s_O + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %s_O { + // TODO: Add response fields here + } + """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent); + + String converterContent = """ + 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; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.converter + * @className %sConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + %s_I toLegacyRequest(%sRequest req); + + @Mapping(source = "phone", target = "phoneNumber") + @Mapping(source = "content", target = "message") + %sRequest toRequest(%s_I mciReq); + + // %sResponse toResponse(%s_O mciRes); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + bizPackage, baseName, author, createDate, createDate, author, + baseName, interfaceId, baseName, + baseName, interfaceId, + baseName, interfaceId + ); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); + log.append("=========================================\n"); + log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n"); + log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n"); + log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n"); + log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n"); + log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n"); + log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n"); + log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); + + if (!clientPrefixCap.isEmpty()) { + String mciClientContent = """ + package %s.%s; + + import org.springframework.stereotype.Component; + import lombok.RequiredArgsConstructor; + import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + + /** + * @package %s.%s + * @className Mci%sClient + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                     * ---------- 개정이력 ----------
+                     * 수정일      수정자    수정내용
+                     * ---------- -------- ---------------------------
+                     * %s  %s    최초생성
+                     * 
+                     * 
+ */ + @Component + @RequiredArgsConstructor + public class Mci%sClient { + private final AxhubMciComponent mci; + + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + return mci.callTo(interfaceId, dummy, mciReq, resType); + } + } + """.formatted( + BASE_PACKAGE, mciGroupPath.replace("/", "."), + BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap + ); + Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent); + log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n"); + } + + } else { + String legacyReqContent = """ + package %s.legacy; + + import lombok.Data; + + /** + * @package %s.legacy + * @className %sLegacyRequest + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyRequest { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent); + + String legacyResContent = """ + package %s.legacy; + + import lombok.Data; + + /** + * @package %s.legacy + * @className %sLegacyResponse + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyResponse { + // TODO: Add legacy response fields here + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent); + + String converterContent = """ + 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.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.converter + * @className %sConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + %sLegacyRequest toLegacyRequest(%sRequest req); + + @Mapping(source = "phone", target = "phoneNumber") + @Mapping(source = "content", target = "message") + %sRequest toRequest(%sLegacyRequest legacyRequest); + + // %sResponse toResponse(%sLegacyResponse legacyResponse); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, author, createDate, createDate, author, + baseName, baseName, baseName, baseName, baseName, baseName, baseName + ); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); + log.append("=========================================\n"); + log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n"); + log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n"); + log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n"); + log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n"); + log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n"); + log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n"); + log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); + } + // schema resource 파일 생성 (useSchemaResource=true 일 때) + if (useSchemaResource) { + Files.createDirectories(schemaDir); + String inputSchema = """ + { + "type": "object", + "additionalProperties": false, + "properties": { + "TODO_FIELD": { + "type": "string", + "description": "TODO: 파라미터 설명을 입력하세요." + } + }, + "required": [] + } + """; + String outputSchema = """ + { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "description": "처리 결과 상태 (SUCCESS / FAILURE)", + "enum": ["SUCCESS", "FAILURE"] + }, + "message": { + "type": "string", + "description": "처리 결과 메시지" + } + }, + "required": ["status"] + } + """; + Files.writeString(schemaDir.resolve(inputSchemaFileName), inputSchema); + Files.writeString(schemaDir.resolve(outputSchemaFileName), outputSchema); + log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n"); + log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n"); + } + + log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); + + return log.toString(); + } + + private static String toKebabCase(String pascalCase) { + if (pascalCase == null || pascalCase.isEmpty()) return pascalCase; + return pascalCase + .replaceAll("([a-z0-9])([A-Z])", "$1-$2") + .toLowerCase(Locale.ROOT); + } + + private static String toToolName(String moduleName, String group, String baseName) { + String moduleDirectory = Path.of(moduleName).getFileName().toString(); + String pod = moduleDirectory.startsWith("dap-was-") + ? moduleDirectory.substring("dap-was-".length()) + : "oth"; + String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2") + .toLowerCase(Locale.ROOT) + .replaceAll("[^a-z0-9]+", " ") + .trim(); + 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( + pod.toLowerCase(Locale.ROOT), + group.toLowerCase(Locale.ROOT), + service, + action); + } + + private static String toPascalCase(String str) { + if (str == null || str.isEmpty()) { + return str; + } + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = true; + for (char c : str.toCharArray()) { + if (c == '_' || c == '-' || c == ' ') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(c); + } + } + if (result.length() > 0) { + result.setCharAt(0, Character.toUpperCase(result.charAt(0))); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java index 12f98a106..ebc4ed136 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java @@ -1,28 +1,14 @@ package io.shinhanlife.dap.lib.util; -/** - * @package io.shinhanlife.dap.lib.util - * @className ToolSchemaResolver - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import io.shinhanlife.dap.lib.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream; +// removed McpOutputSchema +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. */ public class ToolSchemaResolver { @@ -32,13 +18,9 @@ public class ToolSchemaResolver { this.objectMapper = objectMapper; } - public Map resolve(McpFunction function, Class requestType) { - if (function != null && !function.inputSchemaResource().isBlank()) { - return loadResource(function.inputSchemaResource()); - } - if (function != null && !function.inputSchema().isBlank() - && !"{}".equals(function.inputSchema().trim())) { - return parse(function.inputSchema(), "McpFunction.inputSchema"); + public Map resolve(org.springframework.ai.mcp.annotation.McpTool function, ToolHint hint, Class requestType) { + if (hint != null && !hint.inputSchemaResource().isBlank()) { + return loadResource(hint.inputSchemaResource()); } return JsonSchemaGenerator.generateSchema(requestType); } @@ -46,25 +28,36 @@ 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를 분석하여 자동 생성합니다. */ - public Map resolveOutput(McpFunction function, Class responseType) { - if (function != null && !function.outputSchemaResource().isBlank()) { - return loadResource(function.outputSchemaResource()); + public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class responseType, ToolHint hint) { + if (hint != null && !hint.outputSchemaResource().isBlank()) { + return loadResource(hint.outputSchemaResource()); } - if (function != null && !function.outputSchema().isBlank() - && !"{}".equals(function.outputSchema().trim())) { - return parse(function.outputSchema(), "McpFunction.outputSchema"); + return resolveOutput(function, responseType); + } + + /** + * Resolves an explicitly declared response schema. + * Response schemas are opt-in so existing tools keep their current response behavior. + */ + public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class responseType) { + // Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵 + if (responseType == null + || responseType == Object.class + || Map.class.isAssignableFrom(responseType) + || responseType == Void.class + || responseType == void.class) { + return Map.of(); } - if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) { - return JsonSchemaGenerator.generateSchema(responseType); - } - return Map.of(); + return JsonSchemaGenerator.generateSchema(responseType); } /** * Retained for callers that use only explicit output schemas. */ - public Map resolveOutput(McpFunction function) { + public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) { return resolveOutput(function, null); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java new file mode 100644 index 000000000..b1a0796f9 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java @@ -0,0 +1,123 @@ +package io.shinhanlife.dap.lib.util; + + +/** + * @package io.shinhanlife.dap.lib.util + * @className ToolSourceUpdater + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class ToolSourceUpdater { + + public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception { + // 1. Find all *UseCase.java files in dap-was-* directories + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + List javaFiles; + try (Stream paths = Files.walk(rootDir)) { + javaFiles = paths + .filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith("UseCase.java")) + .filter(p -> p.toString().contains("dap-was-") || p.toString().contains("axhub-tool-")) + .collect(Collectors.toList()); + } + + Path targetFile = null; + String content = null; + + // 2. Find the specific file for the tool + String functionName = toolName; + if (toolName.contains("_")) { + functionName = toolName.substring(toolName.indexOf("_") + 1); + } + + Pattern namePattern = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"", Pattern.DOTALL); + Pattern namePattern2 = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(functionName) + "\"", Pattern.DOTALL); + + for (Path path : javaFiles) { + String text = Files.readString(path); + if (namePattern.matcher(text).find()) { + targetFile = path; + content = text; + break; + } else if (namePattern2.matcher(text).find()) { + targetFile = path; + content = text; + toolName = functionName; // Use baseName for subsequent replacements + break; + } + } + + if (targetFile == null) { + throw new Exception("소스 코드를 찾을 수 없습니다: " + toolName); + } + + // 3. Update @McpTool group + if (domainGroup != null && !domainGroup.trim().isEmpty()) { + Pattern groupPattern = Pattern.compile("(@McpTool\\s*\\([^)]*group\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL); + Matcher groupMatcher = groupPattern.matcher(content); + if (groupMatcher.find()) { + content = groupMatcher.replaceFirst("$1" + domainGroup + "$3"); + } + } + + // 4. Update @McpFunction description + if (description != null) { + Pattern funcPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*description\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL); + Matcher funcMatcher = funcPattern.matcher(content); + if (funcMatcher.find()) { + content = funcMatcher.replaceFirst("$1" + description.replace("\\", "\\\\").replace("$", "\\\\$") + "$3"); + } + } + + // 5. Update register flag + Pattern regPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*register\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL); + Matcher regMatcher = regPattern.matcher(content); + if (regMatcher.find()) { + content = regMatcher.replaceFirst("$1" + register + "$3"); + } else { + Pattern addRegPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL); + Matcher addRegMatcher = addRegPattern.matcher(content); + if (addRegMatcher.find()) { + content = addRegMatcher.replaceFirst("$1, register = " + register); + } + } + + // 5.5 Update requiresApproval flag + if (requiresApproval != null) { + Pattern appPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*requiresApproval\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL); + Matcher appMatcher = appPattern.matcher(content); + if (appMatcher.find()) { + content = appMatcher.replaceFirst("$1" + requiresApproval + "$3"); + } else { + Pattern addAppPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL); + Matcher addAppMatcher = addAppPattern.matcher(content); + if (addAppMatcher.find()) { + content = addAppMatcher.replaceFirst("$1, requiresApproval = " + requiresApproval); + } + } + } + + // 6. Write back to file + Files.writeString(targetFile, content); + } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java index e495f517d..414843e38 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.validation; -/** - * @package io.shinhanlife.dap.lib.validation - * @className McpToolNameValidationRunner - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.nio.file.Path; /** Gradle entry point for validating unique MCP Tool names before packaging. */ diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java index 4a64fd8de..f491fd875 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.validation; -/** - * @package io.shinhanlife.dap.lib.validation - * @className McpToolNameValidator - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java index bdea2810c..c138ab077 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.validation; -/** - * @package io.shinhanlife.dap.lib.validation - * @className ToolArgumentSchemaValidator - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.Error; import com.networknt.schema.InputFormat; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java index 9ab099034..44e9f7d9e 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java @@ -2,14 +2,23 @@ package io.shinhanlife.dap.mcc.dto; import lombok.Getter; import lombok.Setter; -import lombok.Builder; -import lombok.NoArgsConstructor; + import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Map; - +import java.util.Set; +import java.util.List; +import java.util.HashSet; +import io.shinhanlife.dap.lib.dto.OperationType; /** - * @package io.shinhanlife.dap.mcc.dto + * Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스 + * Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다. + */ +/** + * @package io.shinhanlife.dap.mcg.dto * @className ToolMetadata * @description AX HUB 시스템 처리 클래스 * @author 0986406 @@ -22,17 +31,16 @@ import java.util.Map; * * */ -@Getter -@Setter @Builder @NoArgsConstructor @AllArgsConstructor +@Getter +@Setter public class ToolMetadata { + // 1. Tool 기본 정보 private String uid; // UUID 형식의 고유 식별자 private String semver; // 버전 (예: 1.0.0) - private Long timeoutMillis; - private Boolean enabled; private String displayName; // 사람이 읽는 라벨 (1-128자) private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool) private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능) @@ -45,18 +53,26 @@ public class ToolMetadata { // 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식) private String categoryKey; + + // 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info) private String endpoint; + + // 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082) private String podUrl; - private String integrationType; - private String mciServiceId; - + // 2-4. 가시성 여부 @Builder.Default private Boolean visible = true; + + // 활성화 여부 + @Builder.Default + private Boolean enabled = true; + // 2-5. Redis 등록 여부 (UI 표출용) @Builder.Default private Boolean isRegistered = true; + // 2-6. HITL 승인 필요 여부 @Builder.Default private Boolean requiresApproval = false; @@ -73,4 +89,45 @@ public class ToolMetadata { private Boolean openWorldHint = false; + + // 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI) + private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI" + + // 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용) + private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992) + + // 5. 인프라 상태 정보 (DIRECT 연동 시 사용) + private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프 + + // 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반) + private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%) + private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수 + private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수 + + // 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입) + @Builder.Default + private OperationType operationType = OperationType.READ; + + @Builder.Default + private Boolean retryEnabled = true; + + @Builder.Default + private Integer circuitBreakerFailureThreshold = 0; + + @Builder.Default + private Long circuitBreakerOpenMillis = 0L; + + @Builder.Default + private Long timeoutMillis = 0L; + + // --- Guardrail 호환성을 위한 메서드 추가 --- + public Set allowedArguments() { + if (parametersSchema == null || !parametersSchema.containsKey("properties")) return Set.of(); + return ((Map) parametersSchema.get("properties")).keySet(); + } + + public Set requiredArguments() { + if (parametersSchema == null || !parametersSchema.containsKey("required")) return Set.of(); + return new HashSet<>((List) parametersSchema.get("required")); + } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java index 770415bbf..c3f42a2b7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java @@ -4,25 +4,24 @@ package io.shinhanlife.dap.mcc.presentation; /** * @package io.shinhanlife.dap.mcc.presentation * @className BusinessToolController - * @description AX HUB ??戮?츩??嶺뚳퐣瑗???????? + * @description AX HUB 시스템 처리 클래스 * @author 0986406 * @create 2026.09.01 *
- * ---------- ?띠룇裕??????----------
- * ??瑜곸젧??     ??瑜곸젧??   ??瑜곸젧??怨몃뮔
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
  * ---------- -------- ---------------------------
- * 2026.09.01  0986406    嶺뚣끉裕???諛댁뎽
+ * 2026.09.01  0986406    최초생성
  * 
  * 
*/ 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.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpTool; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.mcp.LocalToolScanner; +import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import java.lang.reflect.Method; import java.util.ArrayList; @@ -55,17 +54,17 @@ public class BusinessToolController { private final ApplicationContext applicationContext; private final ObjectMapper objectMapper; private final McpProperties mcpProperties; - private final LocalToolScanner localToolScanner; + private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; private final ToolSchemaResolver toolSchemaResolver; - // ???? ?브퀗?????β돦裕뉛쭚?Tool 嶺뚮ㅄ維뽨빳???븐뼔援????? + // 내부 조회용 로컬 Tool 목록 엔드포인트 @GetMapping("/mcp/api/v1/tools/local") public List getLocalTools() { - return localToolScanner.getAllScannedTools(); + return toolRegistryHeartbeatSender.getAllScannedTools(); } - // ??戮?빢 REST ?リ옇?↑€????됱쓤 ??源녿뮡????븐뼔援????? + // 순수 REST 기반 동적 라우팅 엔드포인트 @PostMapping("/mcp/{name}") public ResponseEntity executeDynamicTool( @PathVariable("name") String functionName, @@ -78,25 +77,25 @@ public class BusinessToolController { String finalRequestId = headerRequestId; log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId); - log.info(" [Tool] ???됱쓤 ?????덈뺄 ??븐슙????琉용뼁 (??貫?양춯?: {}", functionName); + log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); if (arguments != null) { try { - log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", objectMapper.writeValueAsString(arguments)); + log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments)); } catch (Exception e) { - log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", arguments); + log.info(" [Tool] 호출 파라미터: {}", arguments); } } Object targetBean = null; Method targetMethod = null; - McpFunction targetFunctionAnnotation = null; + McpTool targetFunctionAnnotation = null; - // McpTool ???????逾???リ옇?↑€??브퀗???뱀쾸? ?熬곣뫁夷????쒖굣?節놁뿉??熬곣뫁逾???????깅さ亦껋깢??? ?熬곣뫕??????????щ뤂????@McpFunction??嶺뚢돦堉????덈펲. + // McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpTool을 찾습니다. Map allBeans = applicationContext.getBeansOfType(Object.class); outerLoop: for (Object bean : allBeans.values()) { Class targetClass = AopUtils.getTargetClass(bean); for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) { - McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class); + McpTool mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpTool.class); if (mcpFunc != null) { String baseName = mcpFunc.name(); String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() @@ -122,7 +121,7 @@ public class BusinessToolController { for (Object bean : allBeans.values()) { Class targetCls = AopUtils.getTargetClass(bean); for (Method m : targetCls.getDeclaredMethods()) { - McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class); + McpTool func = AnnotationUtils.findAnnotation(m, McpTool.class); if (func != null) { String baseName = func.name(); String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() @@ -131,28 +130,29 @@ public class BusinessToolController { } } } - log.error("[Tool] ???덈뺄????貫??Method)??嶺뚢돦堉??????怨룸????덈펲: {}. ?熬곣뫗?????노뼌????嶺뚮∥?꾥땻??嶺뚮ㅄ維뽨빳? {}", functionName, availableFunctions); + log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions); Map errorDetails = new HashMap<>(); errorDetails.put("status", "404"); Map errorBody = new HashMap<>(); errorBody.put("code", "TOOL_NOT_FOUND"); - errorBody.put("message", "???덈뺄????貫???嶺뚢돦堉??????怨룸????덈펲: " + functionName); + 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) + // 2. 파라미터 유효성 검증 (JSON Schema) if (targetMethod.getParameterCount() > 0) { Class paramType = targetMethod.getParameterTypes()[0]; if (!Map.class.isAssignableFrom(paramType)) { try { - Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType); + io.shinhanlife.dap.lib.annotation.ToolHint hint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class); + Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, hint, paramType); List errors = toolArgumentSchemaValidator.validate(inputSchema, arguments); if (!errors.isEmpty()) { - log.error("[Tool] ???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮: {}", errors); + log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); List errorMessages = new ArrayList<>(); for (Error validationError : errors) { errorMessages.add(validationError.getMessage()); @@ -161,28 +161,28 @@ public class BusinessToolController { errorDetails.put("status", "422"); Map errorBody = new HashMap<>(); errorBody.put("code", "INVALID_PARAM"); - errorBody.put("message", "???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮"); + 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.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); } } } - log.info("[Tool] ?洹먮뿫???源끒€?嶺뚯쉳??????덈뺄 -> Method: {}", targetMethod.getName()); + log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName()); try { - // 3. DTO ???逾ф쾬?롮구?????吏?嶺뚮씞?뗩뇡?(Map -> DTO) + // 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()); + log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName()); } } @@ -194,7 +194,8 @@ public class BusinessToolController { methodResult = targetMethod.invoke(targetBean, invokeArgument); } - Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType()); + io.shinhanlife.dap.lib.annotation.ToolHint outputHint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class); + Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType(), outputHint); if (!outputSchema.isEmpty()) { List outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult); if (!outputErrors.isEmpty()) { @@ -212,11 +213,11 @@ public class BusinessToolController { long elapsed = System.currentTimeMillis() - startTime; - // 5. ?롪퍒????꾩룇瑗??(??戮?빢 REST ??얜Ŧ堉? + // 5. 결과 반환 (순수 REST 응답) try { - log.info("[Tool Execution] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); + log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); } catch (Exception e) { - log.info("[Tool Execution] Output Schema Result: {}", methodResult); + log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult); } log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId); @@ -228,7 +229,7 @@ public class BusinessToolController { return responseBuilder.body(methodResult); } catch (Exception e) { - log.error("[Tool] ?洹먮뿫???源끒€????덈뺄 繞????깅뇶 ?꾩룇裕뉑틦? {}", e.getMessage()); + log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); Map errorDetails = new HashMap<>(); errorDetails.put("status", "500"); Map errorBody = new HashMap<>(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java index 3fed10f27..a8eeea7fc 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.mcc.presentation; -/** - * @package io.shinhanlife.dap.mcc.presentation - * @className ToolManifestController - * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 媛쒖젙?대젰 ----------
- * ?섏젙??     ?섏젙??   ?섏젙?댁슜
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    理쒖큹?앹꽦
- * 
- * 
- */ - - import io.shinhanlife.dap.lib.manifest.ToolManifestResponse; import io.shinhanlife.dap.lib.manifest.ToolManifestService; import org.springframework.http.HttpHeaders; @@ -45,4 +29,4 @@ public class ToolManifestController { } return ResponseEntity.ok().eTag(eTag).body(manifest); } -} +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java index 24637daac..9463acf2b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.annotation; -/** - * @package io.shinhanlife.glow.communication.annotation - * @className GlowTrgmField - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java index cc24e3226..7c991d52f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java @@ -1,20 +1,4 @@ package io.shinhanlife.glow.communication.dto; - -/** - * @package io.shinhanlife.glow.communication.dto - * @className CommonHeader - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - import lombok.Data; @Data public class CommonHeader { diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java index 968efe38e..7df3c0a7f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.dto; -/** - * @package io.shinhanlife.glow.communication.dto - * @className HeaderDefaults - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - public enum HeaderDefaults { ITRF_ID, RCV_SVC_ID, STR_YMD, ACNT_OGNZ_NO, PSMR_ASRT_CD, SBSN_RULP_ASRT_CD, BSDU_CD, BSQU_CD, INDV_CTIN_ROLE_CD, SCRN_ID, OGNZ_ASRT_CD, OGNZ_LEVE_CD, TGRM_CREA_CHNN_TYPE_CD, ENVR_TYPE_CD diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java index 1cdcf82c0..31383d775 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.dto; -/** - * @package io.shinhanlife.glow.communication.dto - * @className Transfer - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java index 1cc14890b..464cc52e3 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.exception; -/** - * @package io.shinhanlife.glow.communication.exception - * @className ItrfException - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - public class ItrfException extends Exception { public ItrfException(String msg) { super(msg); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java index 358469627..99bc00be7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.module.eai.component; -/** - * @package io.shinhanlife.glow.communication.module.eai.component - * @className GlowEaiComponent - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.shinhanlife.glow.communication.dto.Transfer; import org.springframework.stereotype.Component; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java index 44bc5d315..ecbe4a963 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.module.mci.component; -/** - * @package io.shinhanlife.glow.communication.module.mci.component - * @className GlowMciComponent - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.shinhanlife.glow.communication.dto.Transfer; import org.springframework.stereotype.Component; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java index d0791306f..fbb154cd6 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java @@ -1,20 +1,4 @@ package io.shinhanlife.glow.communication.util; - -/** - * @package io.shinhanlife.glow.communication.util - * @className CommonHeaderFactory - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - import io.shinhanlife.glow.communication.dto.CommonHeader; import io.shinhanlife.glow.communication.dto.HeaderDefaults; import java.util.Map; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java index e33bcf45b..5216ae1e8 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.util; -/** - * @package io.shinhanlife.glow.util - * @className GlowMciParser - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.shinhanlife.glow.GlowMciFieldInfo; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; diff --git a/dap-was-lib/src/main/resources/glow/application-glow.yml b/dap-was-lib/src/main/resources/glow/application-glow.yml index 97a7d1cc5..508f46c3f 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow.yml @@ -13,7 +13,7 @@ logging: pattern: console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n" -# 2. Redis 공통 설정 (WAS 연동 및 캐싱) +# 2. Redis 공통 설정 (Gateway 연동 및 캐싱) spring: data: redis: diff --git a/dap-was-lib/src/main/resources/static/tool-test-console.html b/dap-was-lib/src/main/resources/static/tool-test-console.html index c6574783c..951eda99c 100644 --- a/dap-was-lib/src/main/resources/static/tool-test-console.html +++ b/dap-was-lib/src/main/resources/static/tool-test-console.html @@ -36,7 +36,7 @@
- AXHUB WAS + AXHUB Gateway