From 782dee0d19ce412442546088bf779abca9ded640 Mon Sep 17 00:00:00 2001 From: jade Date: Fri, 7 Aug 2026 13:07:08 +0900 Subject: [PATCH 01/28] chore: downgrade to spring boot 3.5 --- README.md | 4 +-- build.gradle | 4 +-- dap-gateway/build.gradle | 2 +- .../dap/mcg/presentation/ChatController.java | 2 +- .../RegistryMcpToolSpecificationFactory.java | 29 +++++++++++++---- dap-was-lib/build.gradle | 3 +- .../dap/lib/aop/ToolSlaMonitoringAspect.java | 4 +-- .../lib/mcp/ToolPodMcpToolSynchronizer.java | 32 +++++++++++++++---- .../lib/mcp/ToolRegistryHeartbeatSender.java | 2 +- .../dap/lib/util/JsonSchemaGenerator.java | 2 +- .../dap/lib/util/ToolScaffolder.java | 4 +-- .../dap/lib/util/ToolSchemaResolver.java | 8 ++--- .../presentation/BusinessToolController.java | 2 +- .../dap/lib/util/JsonSchemaGeneratorTest.java | 2 +- .../dap/lib/util/ToolSchemaResolverTest.java | 2 +- .../dap/lib/util/ToolSourceUpdaterTest.java | 2 +- .../validation/McpToolNameValidatorTest.java | 2 +- .../dap/mcc/biz/cmm/dto/BalanceRequest.java | 2 +- .../biz/cmm/dto/BillingProcessRequest.java | 2 +- .../mcc/biz/cmm/dto/BillingStatusRequest.java | 2 +- .../dap/mcc/biz/cmm/dto/BondCheckRequest.java | 2 +- .../dap/mcc/biz/cmm/dto/BondIssueRequest.java | 2 +- .../mcc/biz/cmm/dto/ClaimSearchRequest.java | 2 +- .../mcc/biz/cmm/dto/ClaimSearchResponse.java | 2 +- .../biz/cmm/dto/ContractDetailRequest.java | 2 +- .../biz/cmm/dto/ContractStatusRequest.java | 2 +- .../biz/cmm/dto/CustomerDetailRequest.java | 2 +- .../mcc/biz/cmm/dto/CustomerGradeRequest.java | 2 +- .../mcc/biz/cmm/dto/LeaveCountRequest.java | 2 +- .../biz/cmm/dto/MetaCommonCodeRequest.java | 2 +- .../dap/mcc/biz/cmm/dto/MetaTableRequest.java | 2 +- .../biz/cmm/dto/VacationRegisterRequest.java | 2 +- .../mcc/biz/cmm/usecase/BalanceUseCase.java | 2 +- .../cmm/usecase/BillingProcessUseCase.java | 2 +- .../mcc/biz/cmm/usecase/BondIssueUseCase.java | 2 +- .../ClaimSearchSchemaSampleUseCase.java | 2 +- .../biz/cmm/usecase/CommonUtilityUseCase.java | 2 +- .../cmm/usecase/ContractInquiryUseCase.java | 2 +- .../biz/cmm/usecase/CustomerInfoUseCase.java | 2 +- .../cmm/usecase/MetaCommonCodeUseCase.java | 2 +- .../mcc/biz/cmm/usecase/MetaTableUseCase.java | 2 +- .../cmm/usecase/TemplateUtilityUseCase.java | 2 +- .../mcc/biz/oth/usecase/Onnba3011UseCase.java | 2 +- .../mcc/biz/smp/dto/DailyQuoteRequest.java | 2 +- .../mcc/biz/smp/dto/ExchangeRateRequest.java | 2 +- .../mcc/biz/smp/dto/TeamMemberRequest.java | 2 +- .../dap/mcc/biz/smp/dto/WeatherRequest.java | 2 +- .../smp/usecase/DailyQuoteToolUseCase.java | 2 +- .../smp/usecase/ExchangeRateToolUseCase.java | 2 +- .../biz/smp/usecase/TeamMemberUseCase.java | 2 +- .../biz/smp/usecase/WeatherToolUseCase.java | 2 +- .../mcc/biz/sol/dto/SolReqDetailRequest.java | 2 +- .../mcc/biz/sol/dto/SolReqListRequest.java | 2 +- .../biz/sol/usecase/SolReqDetailUseCase.java | 2 +- .../biz/sol/usecase/SolReqListUseCase.java | 2 +- .../cmm/dto/ClaimSearchRequestSchemaTest.java | 2 +- .../dap/mcc/biz/sms/dto/SmsSendRequest.java | 2 +- .../mcc/biz/sms/usecase/SmsToolUseCase.java | 2 +- 58 files changed, 111 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 5306e961..1b485501 100644 --- a/README.md +++ b/README.md @@ -41,9 +41,9 @@ Gateway는 외부 MCP 진입점과 Tool 등록·라우팅을 담당하고, 실 | 항목 | 기준 | |---|---| | Java | 21 | -| Spring Boot | 4.0.5 | +| Spring Boot | 3.5.11 | | Gradle | 8.14.3 (Wrapper) | -| Spring AI BOM | 2.0.0 | +| Spring AI BOM | 1.1.8 | | 기본 프로필 | `local` | 로컬 환경에서는 민감한 값은 환경 변수로만 주입합니다. diff --git a/build.gradle b/build.gradle index 5f062bcd..8048a91d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.5' apply false + id 'org.springframework.boot' version '3.5.11' apply false id 'io.spring.dependency-management' version '1.1.6' apply false } @@ -26,7 +26,7 @@ subprojects { dependencyManagement { imports { mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES - mavenBom "org.springframework.ai:spring-ai-bom:2.0.0" + mavenBom "org.springframework.ai:spring-ai-bom:1.1.8" } } diff --git a/dap-gateway/build.gradle b/dap-gateway/build.gradle index 87507fb1..66d09a57 100644 --- a/dap-gateway/build.gradle +++ b/dap-gateway/build.gradle @@ -11,7 +11,7 @@ dependencies { // Spring AI MCP Server implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' implementation 'org.springframework.ai:spring-ai-starter-model-openai' - implementation 'org.springframework.ai:spring-ai-openai:2.0.0' + implementation 'org.springframework.ai:spring-ai-openai' // MyBatis & DB implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java index 492407cf..c676aa0d 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java @@ -95,7 +95,7 @@ public class ChatController { .user(message) .tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools() .options(org.springframework.ai.openai.OpenAiChatOptions.builder() - .model(selectedModel)) + .model(selectedModel).build()) .stream() .content(); diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/RegistryMcpToolSpecificationFactory.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/RegistryMcpToolSpecificationFactory.java index 36d3e43c..f4e3a3d3 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/RegistryMcpToolSpecificationFactory.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/RegistryMcpToolSpecificationFactory.java @@ -51,13 +51,14 @@ public class RegistryMcpToolSpecificationFactory { McpSchema.Tool tool = McpSchema.Tool.builder() .name(entry.getName()) .description(description(entry)) - .inputSchema(inputSchema(entry)) - .annotations(McpSchema.ToolAnnotations.builder() - .readOnlyHint(Boolean.TRUE.equals(entry.getReadOnlyHint())) - .destructiveHint(Boolean.TRUE.equals(entry.getDestructiveHint())) - .idempotentHint(Boolean.TRUE.equals(entry.getIdempotentHint())) - .openWorldHint(Boolean.TRUE.equals(entry.getOpenWorldHint())) - .build()) + .inputSchema(toJsonSchema(inputSchema(entry))) + .annotations(new McpSchema.ToolAnnotations( + entry.getDisplayName(), + entry.getReadOnlyHint(), + entry.getDestructiveHint(), + entry.getIdempotentHint(), + entry.getOpenWorldHint(), + null)) .build(); return McpServerFeatures.SyncToolSpecification.builder() @@ -147,6 +148,20 @@ public class RegistryMcpToolSpecificationFactory { return schema; } + @SuppressWarnings("unchecked") + private McpSchema.JsonSchema toJsonSchema(Map schema) { + return new McpSchema.JsonSchema( + String.valueOf(schema.getOrDefault("type", "object")), + schema.get("properties") instanceof Map properties + ? (Map) properties : Map.of(), + schema.get("required") instanceof List required + ? (List) required : List.of(), + schema.get("additionalProperties") instanceof Boolean additionalProperties + ? additionalProperties : Boolean.TRUE, + schema.get("$defs") instanceof Map defs ? (Map) defs : Map.of(), + schema.get("definitions") instanceof Map definitions + ? (Map) definitions : Map.of()); + } private String description(ToolMetadata entry) { return entry.getDescription() == null || entry.getDescription().isBlank() ? entry.getName() + " Tool" diff --git a/dap-was-lib/build.gradle b/dap-was-lib/build.gradle index 2c803d59..69479c76 100644 --- a/dap-was-lib/build.gradle +++ b/dap-was-lib/build.gradle @@ -12,7 +12,7 @@ dependencies { api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0' api 'io.github.resilience4j:resilience4j-ratelimiter' api 'io.github.resilience4j:resilience4j-retry:2.2.0' - api 'org.springframework.boot:spring-boot-starter-aop:3.3.0' + api 'org.springframework.boot:spring-boot-starter-aop' api 'org.springframework.boot:spring-boot-starter-jdbc' api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' @@ -26,6 +26,7 @@ dependencies { api 'com.fasterxml.jackson.core:jackson-databind:2.17.1' api 'com.networknt:json-schema-validator:3.0.0' api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' + api 'org.springaicommunity:mcp-annotations:0.9.0' api 'org.springframework.kafka:spring-kafka:3.2.0' api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' } 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 7408c57d..40be12b5 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 @@ -23,7 +23,7 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; @@ -36,7 +36,7 @@ public class ToolSlaMonitoringAspect { private final McpProperties mcpProperties; // @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다. - @Around("@annotation(org.springframework.ai.mcp.annotation.McpTool)") + @Around("@annotation(org.springaicommunity.mcp.annotation.McpTool)") public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); 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 d9f23f01..b223f0ce 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 @@ -10,6 +10,7 @@ import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext; import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; @@ -46,12 +47,14 @@ public class ToolPodMcpToolSynchronizer { McpSchema.Tool mcpTool = McpSchema.Tool.builder() .name(tool.getName()) .description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription()) - .inputSchema(tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema()) - .annotations(McpSchema.ToolAnnotations.builder() - .readOnlyHint(Boolean.TRUE.equals(tool.getReadOnlyHint())) - .destructiveHint(Boolean.TRUE.equals(tool.getDestructiveHint())) - .idempotentHint(Boolean.TRUE.equals(tool.getIdempotentHint())) - .openWorldHint(Boolean.TRUE.equals(tool.getOpenWorldHint())).build()) + .inputSchema(toJsonSchema(tool.getParametersSchema())) + .annotations(new McpSchema.ToolAnnotations( + tool.getDisplayName(), + tool.getReadOnlyHint(), + tool.getDestructiveHint(), + tool.getIdempotentHint(), + tool.getOpenWorldHint(), + null)) .build(); return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool) .callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build(); @@ -83,4 +86,19 @@ public class ToolPodMcpToolSynchronizer { schema.put("additionalProperties", false); return schema; } -} + @SuppressWarnings("unchecked") + private McpSchema.JsonSchema toJsonSchema(Map source) { + Map schema = source == null ? emptySchema() : source; + return new McpSchema.JsonSchema( + String.valueOf(schema.getOrDefault("type", "object")), + schema.get("properties") instanceof Map properties + ? (Map) properties : Map.of(), + schema.get("required") instanceof List required + ? (List) required : List.of(), + schema.get("additionalProperties") instanceof Boolean additionalProperties + ? additionalProperties : Boolean.TRUE, + schema.get("$defs") instanceof Map defs ? (Map) defs : Map.of(), + schema.get("definitions") instanceof Map definitions + ? (Map) definitions : Map.of()); + } +} \ No newline at end of file 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 index 82965598..ba14d0a8 100644 --- 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 @@ -16,7 +16,7 @@ package io.shinhanlife.dap.lib.mcp; * */ import com.fasterxml.jackson.databind.ObjectMapper; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; 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 1e493804..c01230d0 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,7 +2,7 @@ package io.shinhanlife.dap.lib.util; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import io.swagger.v3.oas.annotations.media.Schema; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; 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 index da0deba8..786efc5a 100644 --- 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 @@ -1,7 +1,7 @@ package io.shinhanlife.dap.lib.util; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -227,7 +227,7 @@ public class ToolScaffolder { String serviceInterfaceContent = """ package %s.usecase; - import org.springframework.ai.mcp.annotation.McpTool; + import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import %s.dto.%sRequest; import %s.dto.%sResponse; 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 ebc4ed13..30207b8a 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 @@ -18,7 +18,7 @@ public class ToolSchemaResolver { this.objectMapper = objectMapper; } - public Map resolve(org.springframework.ai.mcp.annotation.McpTool function, ToolHint hint, Class requestType) { + public Map resolve(org.springaicommunity.mcp.annotation.McpTool function, ToolHint hint, Class requestType) { if (hint != null && !hint.inputSchemaResource().isBlank()) { return loadResource(hint.inputSchemaResource()); } @@ -31,7 +31,7 @@ public class ToolSchemaResolver { * ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고, * 없으면 responseType DTO를 분석하여 자동 생성합니다. */ - public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class responseType, ToolHint hint) { + public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, Class responseType, ToolHint hint) { if (hint != null && !hint.outputSchemaResource().isBlank()) { return loadResource(hint.outputSchemaResource()); } @@ -42,7 +42,7 @@ public class ToolSchemaResolver { * 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) { + public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, Class responseType) { // Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵 if (responseType == null || responseType == Object.class @@ -57,7 +57,7 @@ public class ToolSchemaResolver { /** * Retained for callers that use only explicit output schemas. */ - public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) { + public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function) { return resolveOutput(function, null); } 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 c3f42a2b..602a4a78 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 @@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcc.presentation; */ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.Error; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java index df20d796..27aceba1 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.lib.util; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java index 166c7fad..f30c0e2c 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java @@ -8,7 +8,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; class ToolSchemaResolverTest { diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java index b9c15443..4c0bd514 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java @@ -18,7 +18,7 @@ class ToolSourceUpdaterTest { Files.createDirectories(source.getParent()); Files.writeString(source, """ package example; - import org.springframework.ai.mcp.annotation.McpTool; + import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; interface SampleUseCase { @McpTool(name = "oth.cmm.sample.search", description = "old") diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java index 93e37aee..81ec6e16 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java @@ -90,7 +90,7 @@ class McpToolNameValidatorTest { Files.writeString(source, """ package example; - import org.springframework.ai.mcp.annotation.McpTool; + import org.springaicommunity.mcp.annotation.McpTool; class %s { @McpTool(name = "%s") diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java index aff6ae40..11d70d85 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BalanceRequest.java @@ -5,7 +5,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; /** * @package io.shinhanlife.dap.mcc.dto * @className BalanceRequest diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java index 3d72b062..2895b137 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingProcessRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java index c2cff692..1f655409 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BillingStatusRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java index 28560ecb..85b05402 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondCheckRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java index 69dea77a..7c97d154 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/BondIssueRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java index 093c73e7..6dd3d75d 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java index cd5ed033..9560f6e9 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java index 3fe86720..4c010bfa 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractDetailRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java index 01281c42..06ff6022 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ContractStatusRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java index 92c690d3..a0ae8467 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerDetailRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java index 8ba28a81..04b2e957 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/CustomerGradeRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java index c6af49c0..2ffec703 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/LeaveCountRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java index 5466af2c..78a26a2a 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaCommonCodeRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java index 304dc6cd..cccef9d4 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/MetaTableRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java index 184d0dea..6a120873 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/VacationRegisterRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java index 8935f29b..8e200f8c 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java index 9d0fec93..8c856e1d 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java index 7c92a949..840bd479 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java index c55be3a0..06acfc6b 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java index b7d2d2eb..74f85e62 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java index f3eee3e5..6e21fcb4 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java index 1847f93c..c70d5116 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java index 8ff9ce8a..3829fe0a 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java index 7eec1a6e..03cc0dc1 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java index 63015f61..3b9d4da7 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java index 83d45d03..4d3d06a1 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.oth.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.oth.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java index 36e5e2a6..848b2e84 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/DailyQuoteRequest.java @@ -5,7 +5,7 @@ package io.shinhanlife.dap.mcc.biz.smp.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java index 366c175d..8a2f2fe2 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/ExchangeRateRequest.java @@ -5,7 +5,7 @@ package io.shinhanlife.dap.mcc.biz.smp.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java index ea2314bd..229232e7 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/TeamMemberRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.smp.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java index 9ea49678..3d4e8a78 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/WeatherRequest.java @@ -2,7 +2,7 @@ package io.shinhanlife.dap.mcc.biz.smp.dto; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java index 9905e565..ade68558 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest; import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java index ba628bd5..77cd0ab8 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest; import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java index 5bc204bd..a87cdb19 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java index 1252d6a4..d46d76ca 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java @@ -1,7 +1,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.*; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java index 98d30aa0..0d3e0976 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqDetailRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.sol.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java index 517418ab..e447e69f 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/dto/SolReqListRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.sol.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.Data; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java index 4567e68a..3817d65e 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.sol.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java index 00a7c491..2b14abe4 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.sol.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; diff --git a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java index 10f92c57..5da5877f 100644 --- a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java +++ b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java @@ -13,7 +13,7 @@ import java.lang.reflect.Method; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; class ClaimSearchRequestSchemaTest { diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java index 32bcdbeb..8c635ed6 100644 --- a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/dto/SmsSendRequest.java @@ -4,7 +4,7 @@ package io.shinhanlife.dap.mcc.biz.sms.dto; import io.swagger.v3.oas.annotations.media.Schema; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java index 43156fcf..9f80d439 100644 --- a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java @@ -1,6 +1,6 @@ package io.shinhanlife.dap.mcc.biz.sms.usecase; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sms.dto.*; From 7c9077dbd10cf8c20c32c25ec1943b4efc8e3511 Mon Sep 17 00:00:00 2001 From: jade Date: Fri, 7 Aug 2026 16:56:07 +0900 Subject: [PATCH 02/28] chore: align scaffold gradle with tool standard --- build.gradle | 41 +++++++++++++++--- dap-gateway/build.gradle | 43 ++++++++----------- dap-was-lib/build.gradle | 42 ++++++++++++------ .../dap/lib/util/PodScaffolder.java | 13 +++--- .../dap/lib/util/PodScaffolderTest.java | 26 +++++++++++ dap-was-oth/build.gradle | 7 +-- dap-was-sms/build.gradle | 7 +-- 7 files changed, 120 insertions(+), 59 deletions(-) create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java diff --git a/build.gradle b/build.gradle index 8048a91d..2cd07775 100644 --- a/build.gradle +++ b/build.gradle @@ -1,60 +1,88 @@ +// 루트 Gradle 설정: 모든 Gateway/Tool Pod 모듈이 공유하는 빌드 기준을 정의합니다. plugins { + // Java 컴파일, 테스트, JAR 생성 기능을 제공합니다. id 'java' + + // 하위 실행 모듈에서 bootRun/bootJar를 사용하기 위한 Spring Boot 플러그인입니다. + // 루트 프로젝트에는 적용하지 않으므로 apply false를 사용합니다. id 'org.springframework.boot' version '3.5.11' apply false + + // Spring Boot / Spring AI BOM에 정의된 라이브러리 버전을 일관되게 적용합니다. id 'io.spring.dependency-management' version '1.1.6' apply false } +// 모든 모듈이 공유하는 Maven 식별자입니다. allprojects { group = 'io.shinhanlife' version = '0.0.1-SNAPSHOT' } +// dap-gateway, dap-was-lib, dap-was-oth, dap-was-sms에 공통 적용합니다. subprojects { apply plugin: 'java' apply plugin: 'io.spring.dependency-management' java { + // 프로젝트 표준 Java 버전입니다. sourceCompatibility = '21' } repositories { + // 현재 공개 정식 라이브러리 저장소입니다. + // 폐쇄망 적용 시 사내 Nexus Proxy URL로 교체합니다. mavenCentral() - maven { url 'https://repo.spring.io/milestone' } - maven { url 'https://repo.spring.io/snapshot' } } dependencyManagement { imports { + // Spring Boot 3.5.11과 호환되는 Spring/Jackson/Tomcat 등의 버전을 관리합니다. mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES - mavenBom "org.springframework.ai:spring-ai-bom:1.1.8" + + // Spring AI, MCP, OpenAI 연동 라이브러리의 호환 버전을 1.1.8로 고정합니다. + mavenBom 'org.springframework.ai:spring-ai-bom:1.1.8' } } dependencies { + // Lombok은 컴파일 시 getter/builder 등 반복 코드를 생성하며 실행 JAR에는 포함하지 않습니다. compileOnly 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' + testCompileOnly 'org.projectlombok:lombok:1.18.32' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' + + // Tool DTO와 MCI 요청/응답 객체 간 Converter 구현체를 컴파일 시 자동 생성합니다. implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + + // 모든 모듈의 JUnit 5 기반 테스트 공통 의존성입니다. testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.withType(JavaCompile) { + // 리플렉션/MCP Schema 생성 시 메서드 파라미터명을 사용할 수 있도록 보존합니다. options.compilerArgs << '-parameters' + + // MapStruct 구현체를 Spring Bean으로 생성해 생성자 주입으로 사용할 수 있게 합니다. options.compilerArgs << '-Amapstruct.defaultComponentModel=spring' } tasks.withType(Test) { + // JUnit 5 테스트 플랫폼을 사용합니다. useJUnitPlatform() } } +// Tool 이름 중복 검사 프로그램이 포함된 공통 라이브러리 모듈입니다. def toolCoreProject = project(':dap-was-lib') +// 전체 Tool Pod의 @McpTool(name) 중복을 배포 산출물 생성 전에 차단합니다. tasks.register('validateMcpToolNames', JavaExec) { group = 'verification' - description = 'Checks duplicate @McpFunction names across all Tool modules before packaging.' + description = 'Checks duplicate @McpTool names across all Tool modules before packaging.' + + // 검사 Runner를 실행하기 전에 dap-was-lib 클래스를 먼저 컴파일합니다. dependsOn toolCoreProject.tasks.named('classes') classpath = toolCoreProject.sourceSets.main.runtimeClasspath mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner') @@ -62,7 +90,8 @@ tasks.register('validateMcpToolNames', JavaExec) { } subprojects { + // 배포용 Spring Boot JAR 생성 전에 Tool 이름 중복 검증을 강제합니다. tasks.matching { it.name == 'bootJar' }.configureEach { dependsOn rootProject.tasks.named('validateMcpToolNames') } -} \ No newline at end of file +} diff --git a/dap-gateway/build.gradle b/dap-gateway/build.gradle index 66d09a57..3cd20c29 100644 --- a/dap-gateway/build.gradle +++ b/dap-gateway/build.gradle @@ -1,39 +1,34 @@ plugins { + // Gateway를 독립 실행 가능한 Spring Boot JAR로 생성합니다. id 'org.springframework.boot' } dependencies { + // Tool Registry, MCP 공통 처리, 보안/Schema 유틸리티를 공통 라이브러리에서 가져옵니다. implementation project(':dap-was-lib') - + + // Gateway REST API와 관리 화면의 HTTP 요청을 처리합니다. implementation 'org.springframework.boot:spring-boot-starter-web' + + // Tool Registry 및 분산 캐시 연동에 사용합니다. implementation 'org.springframework.boot:spring-boot-starter-data-redis' - - // Spring AI MCP Server - implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' - implementation 'org.springframework.ai:spring-ai-starter-model-openai' - implementation 'org.springframework.ai:spring-ai-openai' - // MyBatis & DB + + // Tool/Registry 관련 DB 조회와 MyBatis Mapper 실행에 사용합니다. implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' + + // Gateway가 /mcp Endpoint를 MCP Server로 노출하도록 지원합니다. + implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' + + // ChatClient를 통해 OpenAI/OpenRouter 호환 모델을 호출합니다. + implementation 'org.springframework.ai:spring-ai-starter-model-openai' + + // 로컬 개발·테스트용 인메모리 DB입니다. 운영 DB에는 사용하지 않습니다. runtimeOnly 'com.h2database:h2' + + // SQL 로그를 확인하기 위한 JDBC 프록시입니다. implementation 'p6spy:p6spy:3.9.1' - - // MapStruct - implementation "org.mapstruct:mapstruct:1.5.5.Final" - annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0" - annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final" - // implementation 'com.networknt:json-schema-validator:1.4.0' // Spring AI 내장 버전과 충돌 방지를 위해 주석 처리 + // Gateway REST API 문서와 Swagger UI를 제공합니다. implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' -} - -tasks.named('test') { - useJUnitPlatform() -} - -dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' } diff --git a/dap-was-lib/build.gradle b/dap-was-lib/build.gradle index 69479c76..9a2774c0 100644 --- a/dap-was-lib/build.gradle +++ b/dap-was-lib/build.gradle @@ -1,41 +1,55 @@ plugins { + // Gateway와 모든 Tool Pod가 의존하는 공통 라이브러리 모듈입니다. id 'java-library' } dependencies { + // 공통 REST Controller, HTTP Client, 예외 처리 기반입니다. api 'org.springframework.boot:spring-boot-starter-web' + + // Tool Request DTO의 Bean Validation을 지원합니다. api 'org.springframework.boot:spring-boot-starter-validation' + + // Tool Manifest/Registry 캐시 및 Redis 기반 공통 기능을 제공합니다. api 'org.springframework.boot:spring-boot-starter-data-redis' - + + // Tool SLA, 로깅, 공통 Aspect를 적용합니다. + api 'org.springframework.boot:spring-boot-starter-aop' + + // MCI/DB 연동에 필요한 JDBC 공통 기능입니다. + api 'org.springframework.boot:spring-boot-starter-jdbc' + + // Tool 호출의 Circuit Breaker, Rate Limit, Retry 등 복원력 기능입니다. api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0' api 'io.github.resilience4j:resilience4j-core:2.2.0' api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0' api 'io.github.resilience4j:resilience4j-ratelimiter' api 'io.github.resilience4j:resilience4j-retry:2.2.0' - api 'org.springframework.boot:spring-boot-starter-aop' - api 'org.springframework.boot:spring-boot-starter-jdbc' + // MCI/업무 데이터 접근을 위한 MyBatis 지원입니다. api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' + + // 현재 로컬 테스트 DB 및 SQL 로그 처리에 사용합니다. api 'com.h2database:h2' api 'p6spy:p6spy:3.9.1' + + // Spring Data Redis가 사용하는 Redis Client를 공통 모듈에서 직접 참조합니다. api 'io.lettuce:lettuce-core:6.6.0.RELEASE' - api "org.mapstruct:mapstruct:1.5.5.Final" - + // MCI XML 전문, JSON Tool Schema/Manifest 처리에 사용합니다. api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1' api 'com.fasterxml.jackson.core:jackson-databind:2.17.1' api 'com.networknt:json-schema-validator:3.0.0' + + // Tool Pod의 /mcp Streamable HTTP Server와 MCP SDK를 제공합니다. api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc' + + // Spring AI 1.1.x에서 @McpTool, @McpToolParam, metaProvider를 제공합니다. api 'org.springaicommunity:mcp-annotations:0.9.0' + + // 이벤트 기반 확장이 필요한 Tool의 공통 Kafka 연동 기능입니다. api 'org.springframework.kafka:spring-kafka:3.2.0' + + // Tool Pod REST API 문서 및 Swagger UI를 제공합니다. api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' } - -dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testCompileOnly 'org.projectlombok:lombok:1.18.32' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' -} 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 index ea648ea7..decc1a54 100644 --- 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 @@ -46,7 +46,11 @@ public class PodScaffolder { public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException { String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate); + } + + static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName, + String author, String createDate) throws IOException { Path modulePath = rootDir.resolve(Paths.get(moduleName)); if (Files.exists(modulePath)) { return "[오류] 이미 존재하는 모듈입니다: " + moduleName; @@ -59,15 +63,14 @@ public class PodScaffolder { log.append("[2/6] build.gradle 생성 중...\n"); String buildGradle = """ plugins { + // Spring Boot 3.5.11 version is managed by the root build.gradle. id 'org.springframework.boot' } + dependencies { + // Shared MCP, Glow integration, validation, logging, Lombok and MapStruct configuration. implementation project(':dap-was-lib') } - dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' - } """; Files.writeString(modulePath.resolve("build.gradle"), buildGradle); diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java new file mode 100644 index 00000000..cdbfd971 --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java @@ -0,0 +1,26 @@ +package io.shinhanlife.dap.lib.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PodScaffolderTest { + + @TempDir + Path root; + + @Test + void generatesBuildGradleUsingRepositoryStandard() throws Exception { + PodScaffolder.scaffoldPod(root, "dap-was-sample", "8099", "sample", "tester", "2026.08.07"); + + String buildGradle = Files.readString(root.resolve("dap-was-sample/build.gradle")); + assertTrue(buildGradle.contains("id 'org.springframework.boot'")); + assertTrue(buildGradle.contains("Spring Boot 3.5.11")); + assertTrue(buildGradle.contains("implementation project(':dap-was-lib')")); + assertFalse(buildGradle.contains("compileOnly 'org.projectlombok:lombok")); + } +} \ No newline at end of file diff --git a/dap-was-oth/build.gradle b/dap-was-oth/build.gradle index 76871349..93a7fbc1 100644 --- a/dap-was-oth/build.gradle +++ b/dap-was-oth/build.gradle @@ -1,12 +1,9 @@ plugins { + // OTH Tool Pod를 독립 실행 가능한 Spring Boot JAR로 생성합니다. id 'org.springframework.boot' } dependencies { + // MCP Server, Tool 공통 처리, MCI/EAI 연동 기반은 dap-was-lib에서 상속합니다. implementation project(':dap-was-lib') } - -dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' -} diff --git a/dap-was-sms/build.gradle b/dap-was-sms/build.gradle index 76871349..561bc8e3 100644 --- a/dap-was-sms/build.gradle +++ b/dap-was-sms/build.gradle @@ -1,12 +1,9 @@ plugins { + // SMS Tool Pod를 독립 실행 가능한 Spring Boot JAR로 생성합니다. id 'org.springframework.boot' } dependencies { + // MCP Server, Tool 공통 처리, MCI/EAI 연동 기반은 dap-was-lib에서 상속합니다. implementation project(':dap-was-lib') } - -dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' -} From 03203a991278b7ecf8e935f33ef9974211cd2be3 Mon Sep 17 00:00:00 2001 From: jade Date: Sun, 9 Aug 2026 16:11:46 +0900 Subject: [PATCH 03/28] feat: unify MCP tool execution and chat callbacks --- .../dap/mcg/presentation/ChatController.java | 6 +- .../src/main/resources/application.yml | 3 +- .../src/main/resources/static/chat.html | 4 +- .../lib/config/ToolSchemaConfiguration.java | 6 + .../dap/lib/mcp/McpToolExecutionService.java | 136 +++++++++++ .../dap/lib/mcp/ToolExecutionResult.java | 15 ++ .../lib/mcp/ToolPodMcpToolSynchronizer.java | 27 +-- .../lib/mcp/ToolRegistryHeartbeatSender.java | 3 +- .../ToolArgumentSchemaValidator.java | 2 - .../presentation/BusinessToolController.java | 221 ++---------------- .../config/ToolSchemaConfigurationTest.java | 12 +- .../dap/lib/mcp/ToolExecutionServiceTest.java | 60 +++++ .../mcc/manifest/ToolManifestServiceTest.java | 5 +- .../mcc/mcp/McpRequestHeaderFilterTest.java | 23 +- 14 files changed, 266 insertions(+), 257 deletions(-) create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolExecutionResult.java create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java index c676aa0d..acbc6c39 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ChatController.java @@ -75,9 +75,9 @@ public class ChatController { } } - String selectedModel = request.getOrDefault("model", "gemini-flash-latest").trim(); + String selectedModel = request.getOrDefault("model", "cohere/north-mini-code:free").trim(); if (selectedModel.isEmpty()) { - selectedModel = "gemini-flash-latest"; + selectedModel = "cohere/north-mini-code:free"; } // 1. gemini-flash-latest 선택 시 OpenRouter의 Google Gemma 4 모델로 라우팅 @@ -93,7 +93,7 @@ public class ChatController { Flux responseStream = activeChatClient.prompt() .user(message) - .tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools() + .toolCallbacks(callbacks.toArray(new ToolCallback[0])) .options(org.springframework.ai.openai.OpenAiChatOptions.builder() .model(selectedModel).build()) .stream() diff --git a/dap-gateway/src/main/resources/application.yml b/dap-gateway/src/main/resources/application.yml index 3cb3e3a5..0c7afd8e 100644 --- a/dap-gateway/src/main/resources/application.yml +++ b/dap-gateway/src/main/resources/application.yml @@ -13,8 +13,9 @@ spring: api-key: ${OPENROUTER_API_KEY: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} base-url: https://openrouter.ai/api/v1 chat: + completions-path: /chat/completions options: - model: google/gemma-4-31b-it:free + model: cohere/north-mini-code:free temperature: 0.3 server: diff --git a/dap-gateway/src/main/resources/static/chat.html b/dap-gateway/src/main/resources/static/chat.html index 70b21172..f082500c 100644 --- a/dap-gateway/src/main/resources/static/chat.html +++ b/dap-gateway/src/main/resources/static/chat.html @@ -85,11 +85,11 @@
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 0364a5e5..d0f8e2ac 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 @@ -2,6 +2,7 @@ package io.shinhanlife.dap.lib.config; import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,4 +16,9 @@ public class ToolSchemaConfiguration { public ToolSchemaResolver toolSchemaResolver(ObjectMapper objectMapper) { return new ToolSchemaResolver(objectMapper); } + + @Bean + public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper) { + return new ToolArgumentSchemaValidator(objectMapper); + } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java new file mode 100644 index 00000000..bb395f5b --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java @@ -0,0 +1,136 @@ +package io.shinhanlife.dap.lib.mcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.Error; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Service; + +/** Executes a discovered Tool independently from its HTTP or MCP transport. */ +@Slf4j +@Service +@RequiredArgsConstructor +public class McpToolExecutionService { + + private final ApplicationContext applicationContext; + private final ObjectMapper objectMapper; + private final McpProperties mcpProperties; + private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; + private final ToolSchemaResolver toolSchemaResolver; + + public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders, + Map arguments) { + String headerRequestId = requestHeaders == null ? null : requestHeaders.headerRequestId(); + String traceId = requestHeaders == null ? null : requestHeaders.traceId(); + String requestId = requestHeaders == null ? null : requestHeaders.requestId(); + log.info("[Tool] IN - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName); + + ResolvedTool resolvedTool = findTool(functionName); + if (resolvedTool == null) { + return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, headerRequestId); + } + ToolExecutionResult validationFailure = validateInput(resolvedTool, arguments, headerRequestId); + if (validationFailure != null) { + return validationFailure; + } + try { + Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments)); + ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, headerRequestId); + if (outputFailure != null) { + return outputFailure; + } + Map headers = new HashMap<>(); + if (traceId != null) headers.put("trace-id", traceId); + if (requestId != null) headers.put("request-id", requestId); + log.info("[Tool] OUT - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName); + return new ToolExecutionResult(200, methodResult, headers); + } catch (Exception error) { + log.error("[Tool] Tool execution failed. tool={}", functionName, error); + return error(502, "TOOL_ERROR", "Tool execution failed", headerRequestId); + } + } + + private ResolvedTool findTool(String functionName) { + for (Object bean : applicationContext.getBeansOfType(Object.class).values()) { + Class targetClass = AopUtils.getTargetClass(bean); + for (Method declaredMethod : targetClass.getDeclaredMethods()) { + McpTool annotation = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class); + if (annotation != null && matches(functionName, annotation.name())) { + return new ResolvedTool(bean, findInvocableMethod(bean, declaredMethod), annotation, + AnnotationUtils.findAnnotation(declaredMethod, ToolHint.class)); + } + } + } + return null; + } + + private boolean matches(String requestedName, String baseName) { + String namespace = mcpProperties.getNamespace(); + String expectedName = namespace != null && !namespace.isEmpty() ? namespace + "_" + baseName : baseName; + return expectedName.equals(requestedName) || baseName.equals(requestedName); + } + + private Method findInvocableMethod(Object bean, Method declaredMethod) { + try { + return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes()); + } catch (NoSuchMethodException ignored) { + return declaredMethod; + } + } + + private ToolExecutionResult validateInput(ResolvedTool tool, Map arguments, String requestId) { + if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null; + try { + Map schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]); + List errors = toolArgumentSchemaValidator.validate(schema, arguments); + return errors.isEmpty() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId); + } catch (Exception error) { + log.error("[Tool] Input schema validation failed unexpectedly. tool={}", tool.annotation().name(), error); + return null; + } + } + + private Object convertArgument(Method method, Map arguments) { + if (method.getParameterCount() == 0 || arguments == null || Map.class.isAssignableFrom(method.getParameterTypes()[0])) return arguments; + return objectMapper.convertValue(arguments, method.getParameterTypes()[0]); + } + + private Object invoke(ResolvedTool tool, Object argument) throws Exception { + return tool.method().getParameterCount() == 0 ? tool.method().invoke(tool.bean()) : tool.method().invoke(tool.bean(), argument); + } + + private ToolExecutionResult validateOutput(ResolvedTool tool, Object methodResult, String requestId) { + try { + Map outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint()); + if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).isEmpty()) { + return error(500, "INVALID_TOOL_RESPONSE", "Tool response does not match its output schema", requestId); + } + } catch (Exception error) { + log.error("[Tool] Output schema validation failed unexpectedly. tool={}", tool.annotation().name(), error); + } + return null; + } + + private ToolExecutionResult error(int statusCode, String code, String message, String requestId) { + Map body = new HashMap<>(); + body.put("code", code); + body.put("message", message); + body.put("details", Map.of("status", Integer.toString(statusCode))); + if (requestId != null) body.put("request_id", requestId); + return new ToolExecutionResult(statusCode, body, Map.of()); + } + + private record ResolvedTool(Object bean, Method method, McpTool annotation, ToolHint hint) { } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolExecutionResult.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolExecutionResult.java new file mode 100644 index 00000000..461c3f42 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolExecutionResult.java @@ -0,0 +1,15 @@ +package io.shinhanlife.dap.lib.mcp; + +import java.util.Map; + +/** Protocol-neutral result of invoking one Tool Pod business tool. */ +public record ToolExecutionResult(int statusCode, Object body, Map headers) { + + public ToolExecutionResult { + headers = headers == null ? Map.of() : Map.copyOf(headers); + } + + public boolean isSuccess() { + return statusCode >= 200 && statusCode < 300; + } +} 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 b223f0ce..9d327e5a 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 @@ -5,34 +5,29 @@ import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.mcc.presentation.BusinessToolController; -import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; -import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext; -import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; /** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */ @Component -@ConditionalOnBean(BusinessToolController.class) +@ConditionalOnBean(McpToolExecutionService.class) public class ToolPodMcpToolSynchronizer { private final McpSyncServer mcpServer; private final ToolRegistryHeartbeatSender heartbeatSender; - private final BusinessToolController businessToolController; + private final McpToolExecutionService toolExecutionService; private final ObjectMapper objectMapper; public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender, - BusinessToolController businessToolController, ObjectMapper objectMapper) { + McpToolExecutionService toolExecutionService, ObjectMapper objectMapper) { this.mcpServer = mcpServer; this.heartbeatSender = heartbeatSender; - this.businessToolController = businessToolController; + this.toolExecutionService = toolExecutionService; this.objectMapper = objectMapper; } @@ -62,15 +57,9 @@ public class ToolPodMcpToolSynchronizer { private McpSchema.CallToolResult invoke(String toolName, McpRequestHeaders requestHeaders, Map arguments) { - ResponseEntity response = businessToolController.executeDynamicTool( - toolName, - requestHeaders == null ? null : requestHeaders.headerRequestId(), - requestHeaders == null ? null : requestHeaders.traceId(), - requestHeaders == null ? null : requestHeaders.requestId(), - requestHeaders == null ? null : requestHeaders.encryptedEmployeeId(), - arguments); - boolean failed = !response.getStatusCode().is2xxSuccessful(); - Object body = response.getBody(); + ToolExecutionResult result = toolExecutionService.execute(toolName, requestHeaders, arguments); + boolean failed = !result.isSuccess(); + Object body = result.body(); try { return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body)) .structuredContent(body).isError(failed).build(); @@ -101,4 +90,4 @@ public class ToolPodMcpToolSynchronizer { schema.get("definitions") instanceof Map definitions ? (Map) definitions : Map.of()); } -} \ No newline at end of file +} 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 index ba14d0a8..997bb3b5 100644 --- 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 @@ -44,14 +44,13 @@ import org.springframework.util.ClassUtils; import org.springframework.web.client.RestClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import io.shinhanlife.dap.mcc.presentation.BusinessToolController; @Slf4j @Component @Configuration @EnableScheduling @RequiredArgsConstructor -@ConditionalOnBean(BusinessToolController.class) +@ConditionalOnBean(McpToolExecutionService.class) public class ToolRegistryHeartbeatSender { private final ApplicationContext applicationContext; 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 c138ab07..46e54ea6 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 @@ -8,10 +8,8 @@ import com.networknt.schema.SchemaRegistry; import com.networknt.schema.SpecificationVersion; import java.util.List; import java.util.Map; -import org.springframework.stereotype.Component; /** Validates tool arguments with the NetworkNT version selected by the MCP SDK. */ -@Component public class ToolArgumentSchemaValidator { private final ObjectMapper objectMapper; 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 602a4a78..3926a879 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 @@ -1,42 +1,14 @@ package io.shinhanlife.dap.mcc.presentation; - -/** - * @package io.shinhanlife.dap.mcc.presentation - * @className BusinessToolController - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.Error; -import org.springaicommunity.mcp.annotation.McpTool; -import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; -import io.shinhanlife.dap.lib.config.McpProperties; -import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; +import io.shinhanlife.dap.lib.mcp.ToolExecutionResult; +import io.shinhanlife.dap.lib.mcp.McpToolExecutionService; import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; -import io.shinhanlife.dap.lib.util.ToolSchemaResolver; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; import java.util.List; import java.util.Map; -import java.util.Set; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.aop.support.AopUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.ResponseEntity; -import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -45,26 +17,20 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Slf4j +/** Legacy REST adapter for Tool Pod execution. */ @RestController @RequestMapping("/") @RequiredArgsConstructor public class BusinessToolController { - private final ApplicationContext applicationContext; - private final ObjectMapper objectMapper; - private final McpProperties mcpProperties; private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; - private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; + private final McpToolExecutionService toolExecutionService; - private final ToolSchemaResolver toolSchemaResolver; - // 내부 조회용 로컬 Tool 목록 엔드포인트 @GetMapping("/mcp/api/v1/tools/local") public List getLocalTools() { return toolRegistryHeartbeatSender.getAllScannedTools(); } - // 순수 REST 기반 동적 라우팅 엔드포인트 @PostMapping("/mcp/{name}") public ResponseEntity executeDynamicTool( @PathVariable("name") String functionName, @@ -73,173 +39,12 @@ public class BusinessToolController { @RequestHeader(value = "request-id", required = false) String requestId, @RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId, @RequestBody(required = false) Map arguments) { - - String finalRequestId = headerRequestId; - - log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId); - log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); - if (arguments != null) { - try { - log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments)); - } catch (Exception e) { - log.info(" [Tool] 호출 파라미터: {}", arguments); - } - } - Object targetBean = null; - Method targetMethod = null; - McpTool targetFunctionAnnotation = null; - - // McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpTool을 찾습니다. - Map allBeans = applicationContext.getBeansOfType(Object.class); - outerLoop: - for (Object bean : allBeans.values()) { - Class targetClass = AopUtils.getTargetClass(bean); - for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) { - McpTool mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpTool.class); - if (mcpFunc != null) { - String baseName = mcpFunc.name(); - String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() - ? mcpProperties.getNamespace() + "_" + baseName - : baseName; - - if (expectedName.equals(functionName) || baseName.equals(functionName)) { - targetBean = bean; - try { - targetMethod = bean.getClass().getMethod(targetMethodOfClass.getName(), targetMethodOfClass.getParameterTypes()); - } catch (NoSuchMethodException e) { - targetMethod = targetMethodOfClass; - } - targetFunctionAnnotation = mcpFunc; - break outerLoop; - } - } - } - } - - if (targetBean == null || targetMethod == null) { - List availableFunctions = new ArrayList<>(); - for (Object bean : allBeans.values()) { - Class targetCls = AopUtils.getTargetClass(bean); - for (Method m : targetCls.getDeclaredMethods()) { - McpTool func = AnnotationUtils.findAnnotation(m, McpTool.class); - if (func != null) { - String baseName = func.name(); - String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() - ? mcpProperties.getNamespace() + "_" + baseName : baseName; - availableFunctions.add(expName + " (in " + targetCls.getSimpleName() + ")"); - } - } - } - log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions); - Map errorDetails = new HashMap<>(); - errorDetails.put("status", "404"); - Map errorBody = new HashMap<>(); - errorBody.put("code", "TOOL_NOT_FOUND"); - errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName); - errorBody.put("details", errorDetails); - if (finalRequestId != null) errorBody.put("request_id", finalRequestId); - - return ResponseEntity.status(404).body(errorBody); - } - // (기존 차단 로직 제거됨) - - // 2. 파라미터 유효성 검증 (JSON Schema) - if (targetMethod.getParameterCount() > 0) { - Class paramType = targetMethod.getParameterTypes()[0]; - if (!Map.class.isAssignableFrom(paramType)) { - try { - io.shinhanlife.dap.lib.annotation.ToolHint hint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class); - Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, hint, paramType); - List errors = toolArgumentSchemaValidator.validate(inputSchema, arguments); - if (!errors.isEmpty()) { - log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); - List errorMessages = new ArrayList<>(); - for (Error validationError : errors) { - errorMessages.add(validationError.getMessage()); - } - Map errorDetails = new HashMap<>(); - errorDetails.put("status", "422"); - Map errorBody = new HashMap<>(); - errorBody.put("code", "INVALID_PARAM"); - errorBody.put("message", "파라미터 유효성 검증 실패"); - errorBody.put("details", errorDetails); - if (finalRequestId != null) errorBody.put("request_id", finalRequestId); - - return ResponseEntity.status(422).body(errorBody); - } - } catch (Exception e) { - log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); - } - } - } - - log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName()); - - try { - // 3. DTO 파라미터 자동 매핑 (Map -> DTO) - Object invokeArgument = arguments; - if (targetMethod.getParameterCount() > 0 && arguments != null) { - Class paramType = targetMethod.getParameterTypes()[0]; - if (!Map.class.isAssignableFrom(paramType)) { - invokeArgument = objectMapper.convertValue(arguments, paramType); - log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName()); - } - } - - long startTime = System.currentTimeMillis(); - Object methodResult = null; - if (targetMethod.getParameterCount() == 0) { - methodResult = targetMethod.invoke(targetBean); - } else { - methodResult = targetMethod.invoke(targetBean, invokeArgument); - } - - io.shinhanlife.dap.lib.annotation.ToolHint outputHint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class); - Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType(), outputHint); - if (!outputSchema.isEmpty()) { - List outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult); - if (!outputErrors.isEmpty()) { - log.error("[Tool] Output schema validation failed. tool={}, errors={}", - functionName, outputErrors); - Map errorBody = new HashMap<>(); - errorBody.put("code", "INVALID_TOOL_RESPONSE"); - errorBody.put("message", "Tool response does not match its output schema"); - if (finalRequestId != null) { - errorBody.put("request_id", finalRequestId); - } - return ResponseEntity.internalServerError().body(errorBody); - } - } - - long elapsed = System.currentTimeMillis() - startTime; - - // 5. 결과 반환 (순수 REST 응답) - try { - log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); - } catch (Exception e) { - log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult); - } - - log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId); - - ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok(); - if (traceId != null) responseBuilder.header("trace-id", traceId); - if (requestId != null) responseBuilder.header("request-id", requestId); - - return responseBuilder.body(methodResult); - - } catch (Exception e) { - log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); - Map errorDetails = new HashMap<>(); - errorDetails.put("status", "500"); - Map errorBody = new HashMap<>(); - errorBody.put("code", "TOOL_ERROR"); - errorBody.put("message", "Tool execution failed"); - errorDetails.clear(); // Hide details for upstream errors - errorBody.put("details", errorDetails); - if (finalRequestId != null) errorBody.put("request_id", finalRequestId); - - return ResponseEntity.status(502).body(errorBody); - } + ToolExecutionResult result = toolExecutionService.execute( + functionName, + new McpRequestHeaders(headerRequestId, traceId, requestId, encryptedEmployeeId), + arguments); + ResponseEntity.BodyBuilder response = ResponseEntity.status(result.statusCode()); + result.headers().forEach(response::header); + return response.body(result.body()); } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java index efe77023..c33789f2 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java @@ -3,14 +3,20 @@ package io.shinhanlife.dap.lib.config; import static org.junit.jupiter.api.Assertions.assertNotNull; import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; class ToolSchemaConfigurationTest { @Test - void registersToolSchemaResolver() { - ToolSchemaConfiguration configuration = new ToolSchemaConfiguration(); + void providesToolArgumentSchemaValidatorWithoutComponentScanning() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + context.registerBean(ObjectMapper.class); + context.register(ToolSchemaConfiguration.class); + context.refresh(); - assertNotNull(configuration.toolSchemaResolver(new ObjectMapper())); + assertNotNull(context.getBean(ToolArgumentSchemaValidator.class)); + } } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java new file mode 100644 index 00000000..91f98e32 --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java @@ -0,0 +1,60 @@ +package io.shinhanlife.dap.lib.mcp; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpToolParam; +import org.springframework.context.support.StaticApplicationContext; + +class McpToolExecutionServiceTest { + + @Test + void returnsNotFoundWhenNoToolMatchesTheRequestedName() { + McpToolExecutionService service = serviceWith(new Object()); + ToolExecutionResult result = service.execute("missing.cmm.tool.inquiry", null, Map.of()); + assertEquals(404, result.statusCode()); + assertEquals("TOOL_NOT_FOUND", ((Map) result.body()).get("code")); + } + + @Test + void convertsArgumentsToDtoAndExecutesTheMatchedTool() { + McpToolExecutionService service = serviceWith(new EchoTool()); + ToolExecutionResult result = service.execute("sample.cmm.value.echo", + new McpRequestHeaders("gateway-1", "trace-1", "request-1", "employee-1"), Map.of("value", "hello")); + assertEquals(200, result.statusCode()); + assertEquals("hello", ((Map) result.body()).get("value")); + assertEquals("trace-1", result.headers().get("trace-id")); + assertEquals("request-1", result.headers().get("request-id")); + } + + private McpToolExecutionService serviceWith(Object toolBean) { + ObjectMapper objectMapper = new ObjectMapper(); + StaticApplicationContext context = new StaticApplicationContext(); + context.getBeanFactory().registerSingleton("toolBean", toolBean); + context.refresh(); + return new McpToolExecutionService(context, objectMapper, new McpProperties(), + new ToolArgumentSchemaValidator(objectMapper), new ToolSchemaResolver(objectMapper)); + } + + static class EchoTool { + @McpTool(name = "sample.cmm.value.echo", description = "Echoes a value") + @ToolHint + public Map execute(EchoRequest request) { + return Map.of("value", request.value); + } + } + + static class EchoRequest { + @McpToolParam(description = "Value", required = true) + private String value; + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + } +} diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java index 8d09ca36..6efc38ec 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java @@ -1,4 +1,4 @@ -package io.shinhanlife.dap.mcc.manifest; +package io.shinhanlife.dap.lib.manifest; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -6,9 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.dap.lib.config.McpProperties; -import io.shinhanlife.dap.lib.manifest.ToolManifestItem; -import io.shinhanlife.dap.lib.manifest.ToolManifestResponse; -import io.shinhanlife.dap.lib.manifest.ToolManifestService; import io.shinhanlife.dap.mcc.dto.ToolMetadata; import java.util.List; import java.util.Map; diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java index 059174e5..159c27ff 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java @@ -11,12 +11,9 @@ import static org.mockito.Mockito.when; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpSyncServer; import io.shinhanlife.dap.lib.mcp.*; -import io.shinhanlife.dap.mcc.presentation.BusinessToolController; - import java.lang.reflect.Method; import java.util.Map; import org.junit.jupiter.api.Test; -import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -42,22 +39,22 @@ class McpRequestHeaderFilterTest { } @Test - void forwardsCapturedHeadersToBusinessToolExecution() throws Exception { - BusinessToolController controller = mock(BusinessToolController.class); - doReturn(ResponseEntity.ok(Map.of("result", "ok"))) - .when(controller).executeDynamicTool(eq("sampleTool"), eq("gateway-request-id"), eq("trace-001"), - eq("tool-request-001"), eq("encrypted-employee-id"), eq(Map.of("key", "value"))); + void forwardsCapturedHeadersToSharedToolExecutionService() throws Exception { + McpToolExecutionService service = mock(McpToolExecutionService.class); + McpRequestHeaders headers = new McpRequestHeaders( + "gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"); + doReturn(new ToolExecutionResult(200, Map.of("result", "ok"), Map.of())) + .when(service).execute(eq("sampleTool"), eq(headers), eq(Map.of("key", "value"))); ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer( - mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), controller, new ObjectMapper()); + mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), service, new ObjectMapper()); Method invoke = ToolPodMcpToolSynchronizer.class.getDeclaredMethod( "invoke", String.class, McpRequestHeaders.class, Map.class); invoke.setAccessible(true); invoke.invoke(synchronizer, "sampleTool", - new McpRequestHeaders("gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"), + headers, Map.of("key", "value")); - verify(controller).executeDynamicTool("sampleTool", "gateway-request-id", "trace-001", - "tool-request-001", "encrypted-employee-id", Map.of("key", "value")); + verify(service).execute("sampleTool", headers, Map.of("key", "value")); } -} \ No newline at end of file +} From ae00489a39dfe106d5fb239d520245baf4c8d781 Mon Sep 17 00:00:00 2001 From: jade Date: Sun, 9 Aug 2026 16:54:24 +0900 Subject: [PATCH 04/28] feat: align pod scaffolding with oth runtime --- .../dap/lib/util/PodScaffolder.java | 38 +++++++++++++++++-- .../dap/lib/util/ToolScaffolder.java | 13 ++++--- .../dap/lib/util/PodScaffolderTest.java | 21 +++++++++- .../dap/lib/util/ToolScaffolderTest.java | 18 +++++++++ .../ClaimSearchSchemaSampleUseCase.java | 4 +- .../src/main/resources/application-prod.yml | 8 ++++ .../src/main/resources/application-test.yml | 8 ++++ 7 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 dap-was-oth/src/main/resources/application-prod.yml create mode 100644 dap-was-oth/src/main/resources/application-test.yml 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 index decc1a54..c110a636 100644 --- 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 @@ -76,11 +76,14 @@ public class PodScaffolder { log.append("[3/6] Dockerfile 생성 중...\n"); String dockerfile = """ - FROM eclipse-temurin:21-jdk-alpine + FROM eclipse-temurin:21-jre-alpine WORKDIR /app - COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar + RUN apk add --no-cache tzdata + ENV TZ=Asia/Seoul + COPY %s/build/libs/*-SNAPSHOT.jar app.jar + EXPOSE %s ENTRYPOINT ["java", "-jar", "app.jar"] - """.formatted(moduleName); + """.formatted(moduleName, portStr); Files.writeString(modulePath.resolve("Dockerfile"), dockerfile); log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n"); @@ -135,10 +138,13 @@ public class PodScaffolder { org.apache.kafka: ERROR mcp: namespace: "" + manifest: + bundle-id: %s + name-prefix: "" security: tenant-domains: TESTER-DEV: ALL - """.formatted(portStr, moduleName); + """.formatted(portStr, moduleName, moduleName.replace("dap-", "")); Files.writeString(resPath.resolve("application.yml"), applicationYml); String applicationLocalYml = """ @@ -226,6 +232,30 @@ public class PodScaffolder { """.formatted(portStr, portStr); Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml); + String applicationTestYml = """ + server: + port: ${PORT:%s} + + axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} + """.formatted(portStr); + Files.writeString(resPath.resolve("application-test.yml"), applicationTestYml); + + String applicationProdYml = """ + server: + port: ${PORT:%s} + + axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} + """.formatted(portStr); + Files.writeString(resPath.resolve("application-prod.yml"), applicationProdYml); + String logbackXml = """ 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 index 786efc5a..0fe6757e 100644 --- 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 @@ -74,8 +74,9 @@ public class ToolScaffolder { String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): "); boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim()); - String inputSchemaResource = useSchemaResource ? "classpath:mcp/schema/" + toKebabCase(baseName) + "-resource-input-schema.json" : null; - String outputSchemaResource = useSchemaResource ? "classpath:mcp/schema/" + toKebabCase(baseName) + "-resource-output-schema.json" : null; + String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/"; + String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; + String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null; String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, inputSchemaResource, outputSchemaResource); System.out.println(result); @@ -110,9 +111,9 @@ public class ToolScaffolder { String schemaBaseName = toKebabCase(baseName); String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json"; String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json"; - Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mcp/schema")); - String inputSchemaClasspath = "classpath:mcp/schema/" + inputSchemaFileName; - String outputSchemaClasspath = "classpath:mcp/schema/" + outputSchemaFileName; + Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/tool-schemas", group.toLowerCase())); + String inputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + inputSchemaFileName; + String outputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + outputSchemaFileName; String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase(); boolean isMci = "MCI".equalsIgnoreCase(routingType); @@ -784,4 +785,4 @@ public class ToolScaffolder { } return result.toString(); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java index cdbfd971..f7a8807a 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java @@ -23,4 +23,23 @@ class PodScaffolderTest { assertTrue(buildGradle.contains("implementation project(':dap-was-lib')")); assertFalse(buildGradle.contains("compileOnly 'org.projectlombok:lombok")); } -} \ No newline at end of file + + @Test + void generatesOthStandardRuntimeResources() throws Exception { + PodScaffolder.scaffoldPod(root, "dap-was-sample", "8099", "sample", "tester", "2026.08.07"); + + Path resources = root.resolve("dap-was-sample/src/main/resources"); + String dockerfile = Files.readString(root.resolve("dap-was-sample/Dockerfile")); + + assertTrue(dockerfile.contains("RUN apk add --no-cache tzdata")); + assertTrue(dockerfile.contains("ENV TZ=Asia/Seoul")); + assertTrue(dockerfile.contains("COPY dap-was-sample/build/libs/*-SNAPSHOT.jar app.jar")); + assertTrue(dockerfile.contains("EXPOSE 8099")); + assertTrue(Files.exists(resources.resolve("application-test.yml"))); + assertTrue(Files.exists(resources.resolve("application-prod.yml"))); + assertTrue(Files.readString(resources.resolve("application-test.yml")) + .contains("${AXHUB_GATEWAY_URL}")); + assertTrue(Files.readString(resources.resolve("application-prod.yml")) + .contains("${AXHUB_TOOL_URL}")); + } +} diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 46f72a3c..82918a50 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -5,9 +5,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; import java.nio.file.Path; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class ToolScaffolderTest { + @TempDir + Path root; + @Test void generatesManifestReadyToolAndOutputSchemaDto() throws Exception { String moduleName = "build/scaffold-manifest-test"; @@ -38,4 +42,18 @@ class ToolScaffolderTest { assertTrue(useCase.contains("name = \"sms.cmm.notification.send\"")); } + + @Test + void createsSchemaResourcesInToolSchemaCategoryDirectory() throws Exception { + String moduleName = root.resolve("dap-was-sample").toString(); + + ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName, + "tester", "2026.08.09", true, null, + "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", + "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json"); + + Path schemas = root.resolve("dap-was-sample/src/main/resources/tool-schemas/cmm"); + assertTrue(Files.exists(schemas.resolve("claim-search-resource-input-schema.json"))); + assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); + } } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java index 06acfc6b..7eb8bfde 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java @@ -10,7 +10,7 @@ public interface ClaimSearchSchemaSampleUseCase { @McpTool(name = "oth.cmm.claim.search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true)) @ToolHint(register = false, categoryKey = "cmm", - inputSchemaResource = "classpath:mcp/schema/claim-search-resource-input-schema.json", - outputSchemaResource = "classpath:mcp/schema/claim-search-resource-output-schema.json") + inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", + outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json") ClaimSearchResponse search(ClaimSearchRequest request); } diff --git a/dap-was-oth/src/main/resources/application-prod.yml b/dap-was-oth/src/main/resources/application-prod.yml new file mode 100644 index 00000000..21b801dd --- /dev/null +++ b/dap-was-oth/src/main/resources/application-prod.yml @@ -0,0 +1,8 @@ +server: + port: ${PORT:8084} + +axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} diff --git a/dap-was-oth/src/main/resources/application-test.yml b/dap-was-oth/src/main/resources/application-test.yml new file mode 100644 index 00000000..21b801dd --- /dev/null +++ b/dap-was-oth/src/main/resources/application-test.yml @@ -0,0 +1,8 @@ +server: + port: ${PORT:8084} + +axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} From c0567ec1c2be4a2d78d0523ab46184562d847b64 Mon Sep 17 00:00:00 2001 From: jade Date: Sun, 9 Aug 2026 17:13:54 +0900 Subject: [PATCH 05/28] fix: generate schema based tool request DTOs --- .../dap/lib/util/ToolScaffolder.java | 29 +++++++++++++++---- .../dap/lib/util/ToolScaffolderTest.java | 22 ++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) 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 index 0fe6757e..c318202f 100644 --- 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 @@ -1,7 +1,6 @@ package io.shinhanlife.dap.lib.util; -import org.springaicommunity.mcp.annotation.McpToolParam; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -179,6 +178,13 @@ public class ToolScaffolder { private String message; } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + reqContent = reqContent + .replace("import com.fasterxml.jackson.annotation.JsonInclude;", + "import com.fasterxml.jackson.annotation.JsonInclude;\nimport io.swagger.v3.oas.annotations.media.Schema;") + .replaceAll("(?m)^\\s*@McpToolParam\\([^\\r\\n]*\\)\\R", "") + .replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "") + .replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;") + .replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;"); Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); // Generate Response DTO @@ -308,7 +314,7 @@ public class ToolScaffolder { private final %sConverter converter; @Override - public Object execute(%sRequest req) { + public %sResponse execute(%sRequest req) { log.info("[MCI Tool] {} 요청 수신.", "%s"); try { // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) @@ -320,10 +326,18 @@ public class ToolScaffolder { mciReq, Object.class ); - return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS"); + %sResponse response = new %sResponse(); + response.setResultCode("SUCCESS"); + response.setResultMessage(resTransfer.getBody() != null + ? "MCI call completed." + : "MCI call completed without a response body."); + return response; } catch (Exception e) { log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); - return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + %sResponse response = new %sResponse(); + response.setResultCode("ERROR"); + response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error"); + return response; } } } @@ -345,9 +359,14 @@ public class ToolScaffolder { (clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"), baseName, baseName, + baseName, toolName, interfaceId, - interfaceId + interfaceId, + baseName, + baseName, + baseName, + baseName ); } else { serviceImplContent = """ diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 82918a50..39ac1fd0 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -1,5 +1,6 @@ package io.shinhanlife.dap.lib.util; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; @@ -56,4 +57,25 @@ class ToolScaffolderTest { assertTrue(Files.exists(schemas.resolve("claim-search-resource-input-schema.json"))); assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); } + + @Test + void generatesRequestDtoWithSwaggerSchemaInsteadOfMcpToolParam() throws Exception { + String moduleName = root.resolve("dap-was-pay").toString(); + + ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR 조회", "pay", "MCI", moduleName, + "tester", "2026.08.09", true, "DFAG"); + + Path requestPath = root.resolve("dap-was-pay/src/main/java/io/shinhanlife/dap/mcc/biz/pay/dto/SearchHrRequest.java"); + String request = Files.readString(requestPath); + + assertTrue(request.contains("import io.swagger.v3.oas.annotations.media.Schema;")); + assertTrue(request.contains("@Schema(example = \"01012345678\")")); + assertTrue(request.contains("@Schema(example = \"테스트 메시지입니다.\")")); + assertFalse(request.contains("McpToolParam")); + + Path implementationPath = root.resolve("dap-was-pay/src/main/java/io/shinhanlife/dap/mcc/biz/pay/usecase/impl/SearchHrUseCaseImpl.java"); + String implementation = Files.readString(implementationPath); + assertTrue(implementation.contains("public SearchHrResponse execute(SearchHrRequest req)")); + assertTrue(implementation.contains("response.setResultCode(\"SUCCESS\")")); + } } From 813c846f053245c7afa11e18dd77da023e275212 Mon Sep 17 00:00:00 2001 From: jade Date: Sun, 9 Aug 2026 17:50:06 +0900 Subject: [PATCH 06/28] feat: scaffold tools from declared fields --- .../presentation/ScaffoldingController.java | 16 ++- .../main/resources/static/admin/scaffold.html | 50 +++++++++ .../dap/lib/util/ToolScaffolder.java | 103 +++++++++++++++++- .../dap/lib/util/ToolScaffolderTest.java | 29 +++++ 4 files changed, 196 insertions(+), 2 deletions(-) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java index 975a7d79..c905bcf0 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java @@ -18,6 +18,8 @@ package io.shinhanlife.dap.mcg.presentation; import io.shinhanlife.dap.lib.util.PodScaffolder; import io.shinhanlife.dap.lib.util.ToolScaffolder; import io.shinhanlife.dap.lib.util.ToolSourceUpdater; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -66,8 +68,13 @@ public class ScaffoldingController { String clientSystemCode = req.get("clientSystemCode"); String inputSchemaResource = req.get("inputSchemaResource"); String outputSchemaResource = req.get("outputSchemaResource"); + List inputFields = parseFields(req.get("inputFields")); + List outputFields = parseFields(req.get("outputFields")); + if (inputFields.isEmpty()) { + inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false)); + } - return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource); + return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields); } catch (Exception e) { return "오류 발생: " + e.getMessage(); } @@ -107,4 +114,11 @@ public class ScaffoldingController { return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms"); } } + + private List parseFields(String source) throws Exception { + if (source == null || source.isBlank()) { + return List.of(); + } + return new ObjectMapper().readValue(source, new TypeReference>() { }); + } } diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index 1bd6cd9f..976fedba 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -495,6 +495,29 @@
+
+
+
+ +
+ + +
+
+ +
+
+
+ +
+ + +
+
+ +
+
+
@@ -685,6 +708,33 @@ handleFormSubmit('podForm', '/api/v1/scaffold/pod'); handleFormSubmit('toolForm', '/api/v1/scaffold/tool'); + const fieldExamples = { + inputFields: [ + { name: 'employeeId', type: 'String', description: 'Employee identifier', example: 'EMP10001', required: true }, + { name: 'page', type: 'Integer', description: 'Page number', example: '1', required: false } + ], + outputFields: [ + { name: 'employeeName', type: 'String', description: 'Employee name', example: 'Hong Gildong', required: true } + ] + }; + + function loadFieldExample(fieldId) { + document.getElementById(fieldId).value = JSON.stringify(fieldExamples[fieldId], null, 2); + } + + async function copyFieldJson(fieldId) { + const textarea = document.getElementById(fieldId); + const value = textarea.value.trim() || JSON.stringify(fieldExamples[fieldId], null, 2); + try { + await navigator.clipboard.writeText(value); + } catch (error) { + textarea.value = value; + textarea.select(); + document.execCommand('copy'); + textarea.setSelectionRange(0, 0); + } + } + function loadToolList() { const tbody = document.getElementById('toolListBody'); tbody.innerHTML = 'Loading data...'; 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 index c318202f..ac351078 100644 --- 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 @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.Locale; import java.util.Scanner; @@ -41,6 +42,9 @@ public class ToolScaffolder { private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc"; private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc"; + public record FieldDefinition(String name, String type, String description, String example, boolean required) { + } + public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); @@ -94,6 +98,12 @@ public class ToolScaffolder { } public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource) throws IOException { + return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, + register, clientSystemCode, inputSchemaResource, outputSchemaResource, + List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of()); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List inputFields, List outputFields) throws IOException { baseName = toPascalCase(baseName); String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); @@ -185,6 +195,7 @@ public class ToolScaffolder { .replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "") .replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;") .replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;"); + reqContent = dtoContent(bizPackage, baseName + "Request", inputFields, author, createDate, true); Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); // Generate Response DTO @@ -218,6 +229,7 @@ public class ToolScaffolder { // TODO: Add response fields here. Do not include PII in the Tool response. } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + resContent = dtoContent(bizPackage, baseName + "Response", outputFields, author, createDate, false); Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent); String toolName = toToolName(moduleName, group, baseName); @@ -368,6 +380,14 @@ public class ToolScaffolder { baseName, baseName ); + String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", "."); + serviceImplContent = serviceImplContent + .replace("import " + mciIoPackage + "." + interfaceId + "_I;", + "import " + mciIoPackage + "." + interfaceId + "_I;\nimport " + mciIoPackage + "." + interfaceId + "_O;") + .replace("Transfer resTransfer", "Transfer<" + interfaceId + "_O> resTransfer") + .replace("Object.class", interfaceId + "_O.class") + .replace("response.setResultCode(\"SUCCESS\");", + "if (resTransfer.getBody() != null) {\n response = converter.toResponse(resTransfer.getBody());\n }\n response.setResultCode(\"SUCCESS\");"); } else { serviceImplContent = """ package %s.usecase.impl; @@ -461,6 +481,7 @@ public class ToolScaffolder { private String content; } """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate); Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent); String mciResContent = """ @@ -487,6 +508,7 @@ public class ToolScaffolder { // TODO: Add response fields here } """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate); Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent); String converterContent = """ @@ -538,6 +560,7 @@ public class ToolScaffolder { baseName, interfaceId, baseName, interfaceId ); + converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId); Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); log.append("\n=========================================\n"); @@ -579,7 +602,7 @@ public class ToolScaffolder { public class Mci%sClient { private final AxhubMciComponent mci; - public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { return mci.callTo(interfaceId, dummy, mciReq, resType); } } @@ -764,6 +787,84 @@ public class ToolScaffolder { .toLowerCase(Locale.ROOT); } + private static String dtoContent(String packageName, String className, List fields, + String author, String createDate, boolean request) { + String body = fieldLines(fields); + if (!request) { + body = " private String resultCode;\n\n private String resultMessage;\n" + body; + } + return """ + package %s; + + import com.fasterxml.jackson.annotation.JsonInclude; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %s { + %s} + """.formatted(packageName, className, body); + } + + private static String mciIoContent(String packageSuffix, String className, List fields, + String author, String createDate) { + return """ + package %s.%s; + + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; + + @Data + public class %s { + %s} + """.formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields)); + } + + private static String mciConverterContent(String bizPackage, String baseName, String mciPackage, String interfaceId) { + return """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.%s.io.%s_I; + import %s.%s.io.%s_O; + import org.mapstruct.Mapper; + + @Mapper(componentModel = "spring") + public interface %sConverter { + %s_I toLegacyRequest(%sRequest request); + %sRequest toRequest(%s_I mciRequest); + %sResponse toResponse(%s_O mciRes); + } + """.formatted(bizPackage, bizPackage, baseName, bizPackage, baseName, + BASE_PACKAGE, mciPackage, interfaceId, BASE_PACKAGE, mciPackage, interfaceId, + baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId); + } + + private static String fieldLines(List fields) { + StringBuilder source = new StringBuilder(); + for (FieldDefinition field : fields == null ? List.of() : fields) { + String type = supportedType(field.type()); + String description = field.description() == null ? "" : field.description().replace("\"", "\\\""); + String example = field.example() == null ? "" : field.example().replace("\"", "\\\""); + source.append(" @Schema(description = \"").append(description).append("\", example = \"") + .append(example).append("\""); + if (field.required()) { + source.append(", requiredMode = Schema.RequiredMode.REQUIRED"); + } + source.append(")\n private ").append(type).append(' ').append(field.name()).append(";\n\n"); + } + return source.toString(); + } + + private static String supportedType(String type) { + return switch (type == null ? "String" : type) { + case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; + default -> throw new IllegalArgumentException("Unsupported field type: " + type); + }; + } + private static String toToolName(String moduleName, String group, String baseName) { String moduleDirectory = Path.of(moduleName).getFileName().toString(); String pod = moduleDirectory.startsWith("dap-was-") diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 39ac1fd0..05434f10 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -78,4 +79,32 @@ class ToolScaffolderTest { assertTrue(implementation.contains("public SearchHrResponse execute(SearchHrRequest req)")); assertTrue(implementation.contains("response.setResultCode(\"SUCCESS\")")); } + + @Test + void generatesMciToolFromDeclaredInputAndOutputFields() throws Exception { + String moduleName = root.resolve("dap-was-pay").toString(); + List inputFields = List.of( + new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true), + new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", "1", false)); + List outputFields = List.of( + new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong Gildong", true)); + + ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR search", "pay", "MCI", moduleName, + "tester", "2026.08.09", true, "DFAG", null, null, inputFields, outputFields); + + Path sourceRoot = root.resolve("dap-was-pay/src/main/java/io/shinhanlife/dap/mcc"); + String request = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrRequest.java")); + String response = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrResponse.java")); + String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_I.java")); + String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_O.java")); + String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); + + assertTrue(request.contains("private String employeeId;")); + assertTrue(request.contains("private Integer page;")); + assertFalse(request.contains("phoneNumber")); + assertTrue(response.contains("private String employeeName;")); + assertTrue(mciRequest.contains("private String employeeId;")); + assertTrue(mciResponse.contains("private String employeeName;")); + assertTrue(converter.contains("SearchHrResponse toResponse(SHEARCH_01_O mciRes);")); + } } From 6809435cc2e6a789ead5230e8d61637abf94c192 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 09:06:56 +0900 Subject: [PATCH 07/28] feat: cache tool methods on pod startup --- .../dap/lib/mcp/McpToolExecutionService.java | 53 ++--------- .../dap/lib/mcp/McpToolMethodRegistry.java | 87 +++++++++++++++++++ .../lib/mcp/McpToolMethodRegistryTest.java | 57 ++++++++++++ .../dap/lib/mcp/ToolExecutionServiceTest.java | 4 +- 4 files changed, 156 insertions(+), 45 deletions(-) create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistry.java create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java index bb395f5b..6b0c6cfb 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java @@ -2,8 +2,6 @@ package io.shinhanlife.dap.lib.mcp; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.Error; -import io.shinhanlife.dap.lib.annotation.ToolHint; -import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import java.lang.reflect.Method; @@ -12,21 +10,16 @@ import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springaicommunity.mcp.annotation.McpTool; -import org.springframework.aop.support.AopUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Service; -/** Executes a discovered Tool independently from its HTTP or MCP transport. */ +/** Executes a cached Tool independently from its HTTP or MCP transport. */ @Slf4j @Service @RequiredArgsConstructor public class McpToolExecutionService { - private final ApplicationContext applicationContext; + private final McpToolMethodRegistry toolMethodRegistry; private final ObjectMapper objectMapper; - private final McpProperties mcpProperties; private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; private final ToolSchemaResolver toolSchemaResolver; @@ -37,7 +30,7 @@ public class McpToolExecutionService { String requestId = requestHeaders == null ? null : requestHeaders.requestId(); log.info("[Tool] IN - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName); - ResolvedTool resolvedTool = findTool(functionName); + McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName); if (resolvedTool == null) { return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, headerRequestId); } @@ -62,35 +55,8 @@ public class McpToolExecutionService { } } - private ResolvedTool findTool(String functionName) { - for (Object bean : applicationContext.getBeansOfType(Object.class).values()) { - Class targetClass = AopUtils.getTargetClass(bean); - for (Method declaredMethod : targetClass.getDeclaredMethods()) { - McpTool annotation = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class); - if (annotation != null && matches(functionName, annotation.name())) { - return new ResolvedTool(bean, findInvocableMethod(bean, declaredMethod), annotation, - AnnotationUtils.findAnnotation(declaredMethod, ToolHint.class)); - } - } - } - return null; - } - - private boolean matches(String requestedName, String baseName) { - String namespace = mcpProperties.getNamespace(); - String expectedName = namespace != null && !namespace.isEmpty() ? namespace + "_" + baseName : baseName; - return expectedName.equals(requestedName) || baseName.equals(requestedName); - } - - private Method findInvocableMethod(Object bean, Method declaredMethod) { - try { - return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes()); - } catch (NoSuchMethodException ignored) { - return declaredMethod; - } - } - - private ToolExecutionResult validateInput(ResolvedTool tool, Map arguments, String requestId) { + private ToolExecutionResult validateInput(McpToolMethodRegistry.RegisteredTool tool, + Map arguments, String requestId) { if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null; try { Map schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]); @@ -107,11 +73,12 @@ public class McpToolExecutionService { return objectMapper.convertValue(arguments, method.getParameterTypes()[0]); } - private Object invoke(ResolvedTool tool, Object argument) throws Exception { + private Object invoke(McpToolMethodRegistry.RegisteredTool tool, Object argument) throws Exception { return tool.method().getParameterCount() == 0 ? tool.method().invoke(tool.bean()) : tool.method().invoke(tool.bean(), argument); } - private ToolExecutionResult validateOutput(ResolvedTool tool, Object methodResult, String requestId) { + private ToolExecutionResult validateOutput(McpToolMethodRegistry.RegisteredTool tool, + Object methodResult, String requestId) { try { Map outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint()); if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).isEmpty()) { @@ -131,6 +98,4 @@ public class McpToolExecutionService { if (requestId != null) body.put("request_id", requestId); return new ToolExecutionResult(statusCode, body, Map.of()); } - - private record ResolvedTool(Object bean, Method method, McpTool annotation, ToolHint hint) { } -} +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistry.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistry.java new file mode 100644 index 00000000..cd3dc4a1 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistry.java @@ -0,0 +1,87 @@ +package io.shinhanlife.dap.lib.mcp; + +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.lib.config.McpProperties; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springframework.aop.support.AopUtils; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.EventListener; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +/** + * Caches executable {@link McpTool} methods once when a Tool Pod starts. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class McpToolMethodRegistry { + + private final ApplicationContext applicationContext; + private final McpProperties mcpProperties; + + private volatile Map tools = Map.of(); + + @EventListener(ApplicationReadyEvent.class) + public void initialize() { + Map discovered = new LinkedHashMap<>(); + + for (Object bean : applicationContext.getBeansOfType(Object.class).values()) { + Class targetClass = AopUtils.getTargetClass(bean); + for (Method declaredMethod : targetClass.getDeclaredMethods()) { + McpTool annotation = AnnotationUtils.findAnnotation(declaredMethod, McpTool.class); + if (annotation == null) { + continue; + } + + RegisteredTool tool = new RegisteredTool( + bean, + findInvocableMethod(bean, declaredMethod), + annotation, + AnnotationUtils.findAnnotation(declaredMethod, ToolHint.class)); + register(discovered, annotation.name(), tool); + registerNamespaceAlias(discovered, annotation.name(), tool); + } + } + + tools = Map.copyOf(discovered); + log.info("[Tool Registry] {} executable tool names cached", tools.size()); + } + + public RegisteredTool find(String toolName) { + return tools.get(toolName); + } + + private void registerNamespaceAlias(Map discovered, String toolName, + RegisteredTool tool) { + String namespace = mcpProperties.getNamespace(); + if (StringUtils.hasText(namespace)) { + register(discovered, namespace + "_" + toolName, tool); + } + } + + private void register(Map discovered, String toolName, RegisteredTool tool) { + RegisteredTool existing = discovered.putIfAbsent(toolName, tool); + if (existing != null && existing != tool) { + throw new IllegalStateException("Duplicate @McpTool name: " + toolName); + } + } + + private Method findInvocableMethod(Object bean, Method declaredMethod) { + try { + return bean.getClass().getMethod(declaredMethod.getName(), declaredMethod.getParameterTypes()); + } catch (NoSuchMethodException ignored) { + return declaredMethod; + } + } + + public record RegisteredTool(Object bean, Method method, McpTool annotation, ToolHint hint) { + } +} \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java new file mode 100644 index 00000000..050d6b10 --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java @@ -0,0 +1,57 @@ +package io.shinhanlife.dap.lib.mcp; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import io.shinhanlife.dap.lib.config.McpProperties; +import org.junit.jupiter.api.Test; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springframework.context.support.StaticApplicationContext; + +class McpToolMethodRegistryTest { + + @Test + void indexesAnnotatedToolDuringInitialization() { + StaticApplicationContext context = contextWith(new EchoTool()); + McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties()); + + registry.initialize(); + + McpToolMethodRegistry.RegisteredTool tool = registry.find("oth.cmm.echo.search"); + assertNotNull(tool); + assertEquals("execute", tool.method().getName()); + assertEquals("oth.cmm.echo.search", tool.annotation().name()); + } + + @Test + void rejectsDuplicateToolNamesDuringInitialization() { + StaticApplicationContext context = contextWith(new EchoTool(), new DuplicateEchoTool()); + McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties()); + + assertThrows(IllegalStateException.class, registry::initialize); + } + + private StaticApplicationContext contextWith(Object... tools) { + StaticApplicationContext context = new StaticApplicationContext(); + for (int index = 0; index < tools.length; index++) { + context.getBeanFactory().registerSingleton("tool" + index, tools[index]); + } + context.refresh(); + return context; + } + + static class EchoTool { + @McpTool(name = "oth.cmm.echo.search") + public String execute(String request) { + return request; + } + } + + static class DuplicateEchoTool { + @McpTool(name = "oth.cmm.echo.search") + public String execute(String request) { + return request; + } + } +} \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java index 91f98e32..26ddc40a 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java @@ -39,7 +39,9 @@ class McpToolExecutionServiceTest { StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("toolBean", toolBean); context.refresh(); - return new McpToolExecutionService(context, objectMapper, new McpProperties(), + McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties()); + registry.initialize(); + return new McpToolExecutionService(registry, objectMapper, new ToolArgumentSchemaValidator(objectMapper), new ToolSchemaResolver(objectMapper)); } From 7819b11ce3f66a6fc5dfe4fb643ea4ff8863429c Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 09:24:05 +0900 Subject: [PATCH 08/28] feat: opt in to output schema validation --- .../dap/lib/annotation/McpOutputSchema.java | 14 +++++++ .../dap/lib/util/JsonSchemaGenerator.java | 3 +- .../dap/lib/util/ToolSchemaResolver.java | 41 ++++++++----------- .../dap/lib/util/ToolSchemaResolverTest.java | 23 ++++------- 4 files changed, 40 insertions(+), 41 deletions(-) create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java 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 new file mode 100644 index 00000000..6d8f8370 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java @@ -0,0 +1,14 @@ +package io.shinhanlife.dap.lib.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a response DTO whose generated JSON Schema must be exposed and validated for a Tool response. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface McpOutputSchema { +} \ 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 c01230d0..626bbdf2 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 @@ -76,7 +76,8 @@ public class JsonSchemaGenerator { if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) { fieldSchema.put("description", schemaAnnotation.description()); } - if (schemaAnnotation.required() && !requiredList.contains(field.getName())) { + if ((schemaAnnotation.required() || schemaAnnotation.requiredMode() == Schema.RequiredMode.REQUIRED) + && !requiredList.contains(field.getName())) { requiredList.add(field.getName()); } if (!schemaAnnotation.pattern().isEmpty()) { 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 30207b8a..3d7dc3fc 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 @@ -2,14 +2,13 @@ package io.shinhanlife.dap.lib.util; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -// removed McpOutputSchema +import io.shinhanlife.dap.lib.annotation.McpOutputSchema; +import io.shinhanlife.dap.lib.annotation.ToolHint; import java.io.InputStream; import java.util.Map; import org.springframework.core.io.ClassPathResource; -import io.shinhanlife.dap.lib.annotation.ToolHint; - -/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */ +/** Resolves MCP Tool schemas from resources or DTO metadata. */ public class ToolSchemaResolver { private final ObjectMapper objectMapper; @@ -18,7 +17,8 @@ public class ToolSchemaResolver { this.objectMapper = objectMapper; } - public Map resolve(org.springaicommunity.mcp.annotation.McpTool function, ToolHint hint, Class requestType) { + public Map resolve(org.springaicommunity.mcp.annotation.McpTool function, + ToolHint hint, Class requestType) { if (hint != null && !hint.inputSchemaResource().isBlank()) { return loadResource(hint.inputSchemaResource()); } @@ -26,12 +26,11 @@ public class ToolSchemaResolver { } /** - * Resolves an explicitly declared response schema. - * Response schemas are opt-in so existing tools keep their current response behavior. - * ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고, - * 없으면 responseType DTO를 분석하여 자동 생성합니다. + * Resolves a response schema only when it is explicitly declared. + * A JSON resource has precedence over a DTO marker annotation. */ - public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, Class responseType, ToolHint hint) { + public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, + Class responseType, ToolHint hint) { if (hint != null && !hint.outputSchemaResource().isBlank()) { return loadResource(hint.outputSchemaResource()); } @@ -39,16 +38,16 @@ public class ToolSchemaResolver { } /** - * Resolves an explicitly declared response schema. - * Response schemas are opt-in so existing tools keep their current response behavior. + * Generates a response schema only for DTOs marked with {@link McpOutputSchema}. */ - public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, Class responseType) { - // Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵 + public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function, + Class responseType) { if (responseType == null || responseType == Object.class || Map.class.isAssignableFrom(responseType) || responseType == Void.class - || responseType == void.class) { + || responseType == void.class + || !responseType.isAnnotationPresent(McpOutputSchema.class)) { return Map.of(); } return JsonSchemaGenerator.generateSchema(responseType); @@ -58,7 +57,7 @@ public class ToolSchemaResolver { * Retained for callers that use only explicit output schemas. */ public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function) { - return resolveOutput(function, null); + return Map.of(); } private Map loadResource(String location) { @@ -76,12 +75,4 @@ public class ToolSchemaResolver { throw new IllegalStateException("Failed to load MCP schema resource: " + location, e); } } - - private Map parse(String schema, String source) { - try { - return objectMapper.readValue(schema, new TypeReference<>() { }); - } catch (Exception e) { - throw new IllegalStateException("Failed to parse MCP input schema from " + source, e); - } - } -} +} \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java index f30c0e2c..00a72f94 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.McpOutputSchema; +import io.swagger.v3.oas.annotations.media.Schema; import java.lang.reflect.Method; import java.util.List; import java.util.Map; @@ -23,14 +25,6 @@ class ToolSchemaResolverTest { assertTrue(properties(schema).containsKey("differentField")); } - @Test - void resolvesExplicitOutputSchema() throws Exception { - Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); - Map schema = resolver.resolveOutput(method.getAnnotation(McpTool.class)); - assertEquals(false, schema.get("additionalProperties")); - assertTrue(properties(schema).containsKey("resultCode")); - } - @Test void generatesOutputSchemaFromMarkedResponseDto() throws Exception { Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); @@ -45,10 +39,13 @@ class ToolSchemaResolverTest { @Test void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception { Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class); + Map schema = resolver.resolveOutput( method.getAnnotation(McpTool.class), AutomaticRequest.class); + assertTrue(schema.isEmpty()); } + @SuppressWarnings("unchecked") private Map properties(Map schema) { return (Map) schema.get("properties"); @@ -72,19 +69,15 @@ class ToolSchemaResolverTest { } } + @McpOutputSchema static class SimpleResponse { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"SUCCESS", "FAILURE"}) private String resultCode; private String message; } - static class OutputSchemaTool { - @McpTool(name = "oth.test.explicit.search") - void search(AutomaticRequest request) { - } - } - static class AutomaticRequest { private String differentField; } -} +} \ No newline at end of file From dd36247bf7904e237855383a305f695ebdb7b641 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 09:45:13 +0900 Subject: [PATCH 09/28] feat: standardize glow profile configuration --- .../dap/lib/util/PodScaffolder.java | 30 +++++++++++ .../resources/glow/application-glow-dev.yml | 18 ++++++- .../resources/glow/application-glow-local.yml | 18 ++++++- .../resources/glow/application-glow-prod.yml | 18 ++++++- .../resources/glow/application-glow-test.yml | 18 ++++++- .../main/resources/glow/application-glow.yml | 53 +++++++------------ .../dap/lib/util/PodScaffolderTest.java | 8 +++ .../src/main/resources/application-dev.yml | 12 +++-- .../src/main/resources/application-local.yml | 8 ++- .../src/main/resources/application-prod.yml | 8 +++ .../src/main/resources/application-test.yml | 8 +++ .../src/main/resources/application-dev.yml | 12 +++-- .../src/main/resources/application-local.yml | 10 +++- .../src/main/resources/application-prod.yml | 16 ++++++ .../src/main/resources/application-test.yml | 16 ++++++ 15 files changed, 200 insertions(+), 53 deletions(-) create mode 100644 dap-was-sms/src/main/resources/application-prod.yml create mode 100644 dap-was-sms/src/main/resources/application-test.yml 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 index c110a636..09bf3a8e 100644 --- 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 @@ -150,6 +150,12 @@ public class PodScaffolder { String applicationLocalYml = """ # Local 환경 전용 설정 (H2 메모리 DB 등) spring: + config: + activate: + on-profile: local + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-local.yml datasource: url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; driverClassName: com.p6spy.engine.spy.P6SpyDriver @@ -195,6 +201,14 @@ public class PodScaffolder { server: port: ${PORT:%s} + spring: + config: + activate: + on-profile: dev + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-dev.yml + axhub: gateway: url: https://axhubmcp.devjun.net @@ -236,6 +250,14 @@ public class PodScaffolder { server: port: ${PORT:%s} + spring: + config: + activate: + on-profile: test + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-test.yml + axhub: gateway: url: ${AXHUB_GATEWAY_URL} @@ -248,6 +270,14 @@ public class PodScaffolder { server: port: ${PORT:%s} + spring: + config: + activate: + on-profile: prod + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-prod.yml + axhub: gateway: url: ${AXHUB_GATEWAY_URL} diff --git a/dap-was-lib/src/main/resources/glow/application-glow-dev.yml b/dap-was-lib/src/main/resources/glow/application-glow-dev.yml index 693b2abf..e8b864cf 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow-dev.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow-dev.yml @@ -1,5 +1,19 @@ -# Glow 개발 환경 확장 설정 +# Glow 개발 환경 설정 spring: config: activate: - on-profile: dev \ No newline at end of file + on-profile: dev + +glow: + communication: + common: + env-type: D + mci: + host: ${GLOW_COMMUNICATION_MCI_HOST:https://dev-ichmci.shinhanlife.co.kr} + port: ${GLOW_COMMUNICATION_MCI_PORT:26160} + extmci: + host: ${GLOW_COMMUNICATION_EXTMCI_HOST:http://host.docker.internal} + port: ${GLOW_COMMUNICATION_EXTMCI_PORT:8080} + eai: + host: ${GLOW_COMMUNICATION_EAI_HOST:tcp://host.docker.internal} + port: ${GLOW_COMMUNICATION_EAI_PORT:9999} \ No newline at end of file diff --git a/dap-was-lib/src/main/resources/glow/application-glow-local.yml b/dap-was-lib/src/main/resources/glow/application-glow-local.yml index 5db7f933..639fdd02 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow-local.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow-local.yml @@ -1,5 +1,19 @@ -# Glow 로컬 환경 확장 설정 +# Glow 로컬 환경 설정 spring: config: activate: - on-profile: local \ No newline at end of file + on-profile: local + +glow: + communication: + common: + env-type: D + mci: + host: ${GLOW_COMMUNICATION_MCI_HOST:http://localhost} + port: ${GLOW_COMMUNICATION_MCI_PORT:8080} + extmci: + host: ${GLOW_COMMUNICATION_EXTMCI_HOST:http://localhost} + port: ${GLOW_COMMUNICATION_EXTMCI_PORT:8080} + eai: + host: ${GLOW_COMMUNICATION_EAI_HOST:http://localhost} + port: ${GLOW_COMMUNICATION_EAI_PORT:8080} \ No newline at end of file diff --git a/dap-was-lib/src/main/resources/glow/application-glow-prod.yml b/dap-was-lib/src/main/resources/glow/application-glow-prod.yml index 68ddaf64..cf51ecb2 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow-prod.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow-prod.yml @@ -1,5 +1,19 @@ -# Glow 운영 환경 확장 설정 +# Glow 운영 환경 설정 spring: config: activate: - on-profile: prod \ No newline at end of file + on-profile: prod + +glow: + communication: + common: + env-type: P + mci: + host: ${GLOW_COMMUNICATION_MCI_HOST} + port: ${GLOW_COMMUNICATION_MCI_PORT} + extmci: + host: ${GLOW_COMMUNICATION_EXTMCI_HOST} + port: ${GLOW_COMMUNICATION_EXTMCI_PORT} + eai: + host: ${GLOW_COMMUNICATION_EAI_HOST} + port: ${GLOW_COMMUNICATION_EAI_PORT} \ No newline at end of file diff --git a/dap-was-lib/src/main/resources/glow/application-glow-test.yml b/dap-was-lib/src/main/resources/glow/application-glow-test.yml index c0e70f0f..60084090 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow-test.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow-test.yml @@ -1,5 +1,19 @@ -# Glow 테스트 환경 확장 설정 +# Glow 테스트 환경 설정 spring: config: activate: - on-profile: test \ No newline at end of file + on-profile: test + +glow: + communication: + common: + env-type: T + mci: + host: ${GLOW_COMMUNICATION_MCI_HOST} + port: ${GLOW_COMMUNICATION_MCI_PORT} + extmci: + host: ${GLOW_COMMUNICATION_EXTMCI_HOST} + port: ${GLOW_COMMUNICATION_EXTMCI_PORT} + eai: + host: ${GLOW_COMMUNICATION_EAI_HOST} + port: ${GLOW_COMMUNICATION_EAI_PORT} \ No newline at end of file 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 508f46c3..e73d3223 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow.yml @@ -1,9 +1,5 @@ -# ============================================================================== -# [AXHub 공통 환경 설정 파일] (Glow Framework 연동용) -# 이 파일은 axhub-tool-core에 위치하며, 각 툴 파드들이 상속받아 사용합니다. -# ============================================================================== - -# 1. 로깅(Logging) 공통 설정 +# AX HUB Tool Pod 공통 Glow Framework 설정입니다. +# Tool Pod의 application-{profile}.yml에서 이 파일과 환경별 Glow 설정을 함께 import합니다. logging: level: root: INFO @@ -13,42 +9,31 @@ logging: pattern: console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n" -# 2. Redis 공통 설정 (Gateway 연동 및 캐싱) spring: data: redis: - host: 127.0.0.1 # TODO: 실제 Redis 망 IP로 변경 - port: 6379 # TODO: 실제 Redis 포트로 변경 - password: "" # TODO: 실제 비밀번호 입력 + host: ${SPRING_DATA_REDIS_HOST:127.0.0.1} + port: ${SPRING_DATA_REDIS_PORT:6379} + password: ${SPRING_DATA_REDIS_PASSWORD:} -# 3. 신한라이프 Glow Framework 통신 (MCI / EAI) 설정 glow: - communication: #Communication 설정 - common: - env-type: D # 대내표준 헤더의 환경 타입정보 (D: 개발, T: 테스트, P: 운영) - http: # HTTP 비표준 통신 설정 - connection-timeout: 5 # 연결 타임아웃 시간 (초 단위) - read-timeout: 5 # 읽기 타임아웃 시간 (초 단위) - mci: # 개발계 대내 MCI 통신 설정 - host: https://dev-ichmci.shinhanlife.co.kr - port: 26160 + communication: + http: + connection-timeout: 5 + read-timeout: 5 + mci: uri: /ntl_mci/dap_rcv receive-uri: /itrf/mciReceive connection-timeout: 300 read-timeout: 300 encoding: UTF-8 - extmci: # 대외MCI 통신 설정 - host: http://host.docker.internal # 대외MCI 호스트 주소 - port: 8080 # 대외MCI 포트 번호 - uri: /extmci # 대외MCI URI 정보 - json-uri: /extmciJson # 대외MCI JSON방식 URI 정보 - receive-uri: /app/extMciReceive # 대외MCI 수신 URI - connection-timeout: 5 # 연결 타임아웃 시간 (초 단위) - read-timeout: 30 # 읽기 타임아웃 시간 (초 단위) - encoding: EUC-KR # 대외MCI 인코딩 정보 - eai: # EAI 통신 설정 - host: tcp://host.docker.internal # EAI 호스트 주소 (TODO: 실제 주소로 변경) - port: 9999 # EAI 포트 번호 + extmci: + uri: /extmci + json-uri: /extmciJson + receive-uri: /app/extMciReceive + connection-timeout: 5 + read-timeout: 30 + encoding: EUC-KR websocket: - endpoint: "/ws-glow" - allowed-origins: "*" + endpoint: /ws-glow + allowed-origins: "*" \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java index f7a8807a..5d708d4b 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java @@ -41,5 +41,13 @@ class PodScaffolderTest { .contains("${AXHUB_GATEWAY_URL}")); assertTrue(Files.readString(resources.resolve("application-prod.yml")) .contains("${AXHUB_TOOL_URL}")); + assertTrue(Files.readString(resources.resolve("application-local.yml")) + .contains("classpath:glow/application-glow-local.yml")); + assertTrue(Files.readString(resources.resolve("application-dev.yml")) + .contains("classpath:glow/application-glow-dev.yml")); + assertTrue(Files.readString(resources.resolve("application-test.yml")) + .contains("classpath:glow/application-glow-test.yml")); + assertTrue(Files.readString(resources.resolve("application-prod.yml")) + .contains("classpath:glow/application-glow-prod.yml")); } } diff --git a/dap-was-oth/src/main/resources/application-dev.yml b/dap-was-oth/src/main/resources/application-dev.yml index fa5ebf30..b1a82a17 100644 --- a/dap-was-oth/src/main/resources/application-dev.yml +++ b/dap-was-oth/src/main/resources/application-dev.yml @@ -6,6 +6,14 @@ axhub: gateway: url: https://axhubmcp.devjun.net +spring: + config: + activate: + on-profile: dev + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-dev.yml + eims: http: url: http://localhost:${server.port}/api/gateway @@ -22,7 +30,3 @@ eims: url: http://localhost:${server.port}/api/mock/esb/api mcistring: url: http://localhost:${server.port}/api/mock/esb/string - -spring: - config: - import: optional:classpath:glow/application-glow.yml diff --git a/dap-was-oth/src/main/resources/application-local.yml b/dap-was-oth/src/main/resources/application-local.yml index a4e5f24c..1e73a06d 100644 --- a/dap-was-oth/src/main/resources/application-local.yml +++ b/dap-was-oth/src/main/resources/application-local.yml @@ -1,5 +1,11 @@ -# Local ?섍꼍 ?꾩슜 ?ㅼ젙 (H2 硫붾え由?DB ?? +# Local 환경 전용 설정 (H2 메모리 DB 등) spring: + config: + activate: + on-profile: local + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-local.yml datasource: url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; driverClassName: com.p6spy.engine.spy.P6SpyDriver diff --git a/dap-was-oth/src/main/resources/application-prod.yml b/dap-was-oth/src/main/resources/application-prod.yml index 21b801dd..b5f08f21 100644 --- a/dap-was-oth/src/main/resources/application-prod.yml +++ b/dap-was-oth/src/main/resources/application-prod.yml @@ -1,6 +1,14 @@ server: port: ${PORT:8084} +spring: + config: + activate: + on-profile: prod + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-prod.yml + axhub: gateway: url: ${AXHUB_GATEWAY_URL} diff --git a/dap-was-oth/src/main/resources/application-test.yml b/dap-was-oth/src/main/resources/application-test.yml index 21b801dd..ab1c6908 100644 --- a/dap-was-oth/src/main/resources/application-test.yml +++ b/dap-was-oth/src/main/resources/application-test.yml @@ -1,6 +1,14 @@ server: port: ${PORT:8084} +spring: + config: + activate: + on-profile: test + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-test.yml + axhub: gateway: url: ${AXHUB_GATEWAY_URL} diff --git a/dap-was-sms/src/main/resources/application-dev.yml b/dap-was-sms/src/main/resources/application-dev.yml index 4020bb26..638b9e58 100644 --- a/dap-was-sms/src/main/resources/application-dev.yml +++ b/dap-was-sms/src/main/resources/application-dev.yml @@ -6,6 +6,14 @@ axhub: gateway: url: https://axhubmcp.devjun.net +spring: + config: + activate: + on-profile: dev + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-dev.yml + eims: http: url: http://localhost:${server.port}/api/gateway @@ -22,7 +30,3 @@ eims: url: http://localhost:${server.port}/api/mock/esb/api mcistring: url: http://localhost:${server.port}/api/mock/esb/string - -spring: - config: - import: optional:classpath:glow/application-glow.yml diff --git a/dap-was-sms/src/main/resources/application-local.yml b/dap-was-sms/src/main/resources/application-local.yml index 13593bf4..caa93c7f 100644 --- a/dap-was-sms/src/main/resources/application-local.yml +++ b/dap-was-sms/src/main/resources/application-local.yml @@ -1,5 +1,11 @@ -# Local ?섍꼍 ?꾩슜 ?ㅼ젙 (H2 硫붾え由?DB ?? +# Local 환경 전용 설정 (H2 메모리 DB 등) spring: + config: + activate: + on-profile: local + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-local.yml datasource: url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; driverClassName: com.p6spy.engine.spy.P6SpyDriver @@ -36,4 +42,4 @@ axhub: gateway: url: http://localhost:8081 tool: - url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} \ No newline at end of file + url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} diff --git a/dap-was-sms/src/main/resources/application-prod.yml b/dap-was-sms/src/main/resources/application-prod.yml new file mode 100644 index 00000000..8d6f1ab0 --- /dev/null +++ b/dap-was-sms/src/main/resources/application-prod.yml @@ -0,0 +1,16 @@ +server: + port: ${PORT:8082} + +spring: + config: + activate: + on-profile: prod + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-prod.yml + +axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} \ No newline at end of file diff --git a/dap-was-sms/src/main/resources/application-test.yml b/dap-was-sms/src/main/resources/application-test.yml new file mode 100644 index 00000000..a394e88d --- /dev/null +++ b/dap-was-sms/src/main/resources/application-test.yml @@ -0,0 +1,16 @@ +server: + port: ${PORT:8082} + +spring: + config: + activate: + on-profile: test + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-test.yml + +axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} \ No newline at end of file From 906e7e206d172ba119550bad9da28d7833aa0e1b Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 10:29:53 +0900 Subject: [PATCH 10/28] fix: fall back to tool pods when redis is unavailable --- .../mcg/presentation/McpRouterController.java | 15 +++++--- .../presentation/McpRouterControllerTest.java | 38 +++++++++++++++++++ 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/McpRouterControllerTest.java diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java index 99773b7a..3ef467de 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java @@ -113,11 +113,16 @@ public class McpRouterController { } private List fetchAllActiveTools() { - List activeTools = redisRegistryService.getAllTools() - .stream() - .filter(ToolMetadata::getVisible) - .collect(Collectors.toList()); - + List activeTools = new ArrayList<>(); + try { + activeTools.addAll(redisRegistryService.getAllTools() + .stream() + .filter(ToolMetadata::getVisible) + .collect(Collectors.toList())); + } catch (org.springframework.data.redis.RedisConnectionFailureException exception) { + log.warn("Redis is unavailable. Fetching tools from configured fallback Tool Pods instead."); + } + Set knownTools = activeTools.stream() .map(ToolMetadata::getUid) .collect(Collectors.toSet()); diff --git a/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/McpRouterControllerTest.java b/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/McpRouterControllerTest.java new file mode 100644 index 00000000..d5a1a84f --- /dev/null +++ b/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/McpRouterControllerTest.java @@ -0,0 +1,38 @@ +package io.shinhanlife.dap.mcg.presentation; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.mcp.security.SecurityProperties; +import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse; +import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties; +import io.shinhanlife.dap.mcg.registry.RedisRegistryService; +import io.shinhanlife.dap.mcg.service.ExecuteService; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.RedisConnectionFailureException; +import org.springframework.http.ResponseEntity; + +class McpRouterControllerTest { + + @Test + void returnsEmptyToolListWhenRedisIsUnavailableAndNoFallbackIsConfigured() { + RedisRegistryService registryService = org.mockito.Mockito.mock(RedisRegistryService.class); + when(registryService.getAllTools()).thenThrow(new RedisConnectionFailureException("Redis unavailable")); + + McpRouterController controller = new McpRouterController( + registryService, + org.mockito.Mockito.mock(ExecuteService.class), + org.mockito.Mockito.mock(SecurityProperties.class), + new ObjectMapper(), + new GatewayFallbackProperties()); + + ResponseEntity response = assertDoesNotThrow(() -> controller.listTools(null)); + Map result = (Map) response.getBody().getResult(); + + assertEquals(List.of(), result.get("tools")); + } +} \ No newline at end of file From bc5f224e22da511dbadbe88e8a894e3806ed8d90 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 10:48:17 +0900 Subject: [PATCH 11/28] docs: add internal network migration checklist --- ...fe-internal-network-migration-checklist.md | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 docs/shinhanlife-internal-network-migration-checklist.md diff --git a/docs/shinhanlife-internal-network-migration-checklist.md b/docs/shinhanlife-internal-network-migration-checklist.md new file mode 100644 index 00000000..0d09894a --- /dev/null +++ b/docs/shinhanlife-internal-network-migration-checklist.md @@ -0,0 +1,96 @@ +# 신한라이프 내부망 Tool Pod 이관 준비 체크리스트 + +> 범위: `dap-was-lib`, `dap-was-oth`, `dap-was-sms` Tool Pod 이관 +> + +## 1. 소스 및 형상관리 + +- [ ] 내부 Git 저장소 생성 +- [ ] 대상 모듈 이관: `dap-was-lib`, `dap-was-oth`, `dap-was-sms` +- [ ] `main` 브랜치 및 필요한 커밋 이력 이관 +- [ ] API Key, 비밀번호, 인증서, 개인 설정 파일은 Git에서 제외 +- [ ] 내부 Git URL 기준으로 README와 CI/CD 설정 변경 + +## 2. 개발 및 빌드 환경 + +- [ ] JDK 21 설치 및 `JAVA_HOME` 설정 +- [ ] Gradle Wrapper 실행 가능 여부 확인 +- [ ] Docker 또는 Kubernetes 배포 환경 확인 +- [ ] 내부 Git 접근 권한 확인 +- [ ] 사내 Nexus 접근 권한 확인 + +## 3. 사내 Nexus 라이브러리 준비 + +인터넷 없이 빌드하려면 사내 Nexus에 다음 라이브러리 또는 Proxy Repository가 준비되어야 합니다. + +- [ ] Spring Boot 3.5.11 +- [ ] Spring AI 1.1.8 +- [ ] MapStruct, Lombok, H2, P6Spy +- [ ] Glow Framework 관련 라이브러리 +- [ ] 신한라이프 MCI/EAI 및 보안 관련 사내 라이브러리 +- [ ] Docker Base Image: `eclipse-temurin:21-jre-alpine` + +## 4. 환경별 설정 + +사용 프로필: + +```text +local / dev / test / prod +``` + +배포 환경변수: + +```text +SPRING_PROFILES_ACTIVE +AXHUB_TOOL_URL + +GLOW_COMMUNICATION_MCI_HOST +GLOW_COMMUNICATION_MCI_PORT +``` + +- [ ] 개발계(`dev`) MCI 주소 및 포트 등록 +- [ ] 테스트계(`test`) MCI 주소 및 포트 등록 +- [ ] 운영계(`prod`) MCI 주소 및 포트 등록 +- [ ] 운영 비밀값은 Git이 아닌 배포 환경변수 또는 Secret으로 관리 + +## 5. MCI 연계 협의 + +MCI 담당자에게 아래 정보를 요청합니다. + +- [ ] 개발·테스트·운영 MCI URL과 포트 +- [ ] Interface ID와 URI +- [ ] 요청·응답 전문 규격 +- [ ] 필수 Header 및 인증 방식 +- [ ] Connection/Read Timeout 기준 +- [ ] Tool Pod → MCI 통신 ACL 허용 + +## 6. Tool Pod 배포 정보 + +| Tool Pod | 내부 Endpoint | +|---|---| +| OTH Tool Pod | `http://tool-oth:8084/mcp` | +| SMS Tool Pod | `http://tool-sms:8082/mcp` | + +Portal 담당자에게 아래 정보를 전달합니다. + +- [ ] Pod명 및 서비스명 +- [ ] 포트와 MCP Endpoint +- [ ] Tool 목록 및 Tool 명칭 +- [ ] 담당자 및 장애 연락처 +- [ ] Health Check URL + +## 7. Redis 정책 + +- [ ] Tool Pod에서 Redis 사용 여부 결정 +- [ ] Redis를 사용할 경우 Host, Port, Password, ACL 확인 +- [ ] Redis를 사용하지 않을 경우 Tool Pod 기능에 영향이 없는지 개별 Tool 기준 확인 + +## 8. 이관 후 검증 + +- [ ] 내부 Nexus만으로 `./gradlew clean build` 성공 +- [ ] OTH, SMS Tool Pod 기동 성공 +- [ ] 각 Tool Pod의 MCP Endpoint 연결 성공 +- [ ] Tool Pod → MCI 호출 성공 +- [ ] `trace-id`, `request-id` 전달 확인 +- [ ] Tool 이름 중복 검증 확인 +- [ ] 민감정보와 비밀값이 Git에 포함되지 않았는지 확인 \ No newline at end of file From 2f643a626ce8b5c5db835ac9a53f13f19f14e459 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 11:05:04 +0900 Subject: [PATCH 12/28] docs: clarify internal java image usage in dockerfile --- Dockerfile | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 07ff5ffa..e113bd2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,50 @@ -# 1. ??���???�꼍 (JDK 21) +# 1. 빌드 환경 (JDK 21) +# ----------------------------------------------------------------------------- +# 외부 인터넷이 차단된 내부망에서는 Docker Hub 대신 사내 Container Registry의 +# Java 21 이미지를 사용합니다. 인프라 담당자에게 이미지의 전체 경로와 태그를 +# 받은 후 아래 FROM 행만 교체합니다. +# +# 예: FROM registry.shinhanlife.co.kr/base/openjdk:21 AS builder +# +# 이 단계는 Gradle 빌드와 Java 컴파일을 수행하므로 반드시 JDK 21 이미지여야 합니다. +# 사내 이미지가 실제로 JDK 21인지 다음 명령으로 확인합니다. +# docker run --rm <사내-JDK-이미지> java -version FROM eclipse-temurin:21-jdk-alpine AS builder WORKDIR /app -# Gradle ??�띁?? ???�� 蹂듭�? +# Gradle Wrapper와 소스 파일을 복사합니다. COPY gradlew . COPY gradle gradle COPY build.gradle settings.gradle ./ COPY src src -# 沅뚰�??�€??�???���?(???��????�쇅) +# Wrapper 실행 권한을 부여하고 테스트를 제외한 빌드를 수행합니다. RUN chmod +x gradlew RUN ./gradlew clean build -x test -# 2. ??�뻾 ??�꼍 (JRE 21) +# 2. 실행 환경 (JRE 21) +# ----------------------------------------------------------------------------- +# 실행 단계는 JRE 21 이미지가 가장 가볍지만, 사내에서 JDK 21 이미지만 제공하는 +# 경우에는 동일한 JDK 21 이미지를 사용해도 정상 동작합니다. +# +# 예: FROM registry.shinhanlife.co.kr/base/openjre:21 +# 예: FROM registry.shinhanlife.co.kr/base/openjdk:21 +# +# 사내 제공 이미지의 기반 OS를 확인합니다. +# - Alpine 기반: 아래 apk 명령을 그대로 사용합니다. +# - Ubuntu/Debian 기반: apk 대신 apt-get update && apt-get install -y tzdata를 사용합니다. FROM eclipse-temurin:21-jre-alpine WORKDIR /app -# ???꾩�???�젙 (??�뎅 ??�컙) +# 서울 시간대를 설정합니다. RUN apk add --no-cache tzdata ENV TZ=Asia/Seoul -# ??���??JAR ???�� 蹂듭�? +# 빌드 단계에서 생성한 애플리케이션 JAR를 복사합니다. COPY --from=builder /app/build/libs/*.jar app.jar -# 湲곕???????몄텧 +# 애플리케이션 포트를 노출합니다. EXPOSE 8081 -# ?�⑦???��???�뻾 ??JAR ??�뻾 -ENTRYPOINT ["java", "-jar", "app.jar"] - +# 애플리케이션을 실행합니다. +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file From 5fa086531dfa2bac821827e4239b9b56fc88d92a Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:29:23 +0900 Subject: [PATCH 13/28] fix: change serverInfo prefix from DAP-Gateway to dap-was in DynamicMcpServerManager --- .../io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java index e93a2e9d..54cdf044 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java @@ -68,7 +68,7 @@ public class DynamicMcpServerManager { CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper); McpSyncServer newServer = McpServer.sync(transport) - .serverInfo("DAP-Gateway-" + key, "1.0.0") + .serverInfo("dap-was-" + key, "1.0.0") .capabilities(ServerCapabilities.builder().tools(true).build()) .build(); From 25f55e44552865d083d196b9ca327326e851fcdc Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:36:55 +0900 Subject: [PATCH 14/28] fix: uncomment emitter.complete() to close stream for custom protocol --- .../dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java index 44cfceae..05d7b1a8 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java @@ -220,7 +220,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor // Custom 프로토콜: 1회 요청당 1응답 후 종료 (스트림을 닫아버림) // 클라이언트가 한 번의 POST 후 응답을 받고 연결을 끊기 때문 - // this.emitter.complete(); + this.emitter.complete(); } } catch (Exception e) { log.error("Error sending message to SSE emitter", e); From cf0a25bac3a4b89deb08c7e8e73d69bca3344e66 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:40:48 +0900 Subject: [PATCH 15/28] fix: uncomment emitter.complete() for notifications to prevent client hang --- .../dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java index 05d7b1a8..5ca89646 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/CustomWebMvcSseServerTransportProvider.java @@ -179,7 +179,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor log.info("Message sent to session handler"); if (emitter != null && !map.containsKey("id")) { - // emitter.complete(); + emitter.complete(); log.info("Completed emitter for notification (disabled for keep-alive)"); } From 4d929da2ce028d1b59a7eb3dab8e9b374a6b212a Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:44:58 +0900 Subject: [PATCH 16/28] fix: set register=true for Onnba3011UseCase to expose it to MCP Gateway --- .../shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java index 4d3d06a1..7f8b3654 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java @@ -6,6 +6,6 @@ import io.shinhanlife.dap.mcc.biz.oth.dto.*; public interface Onnba3011UseCase { @McpTool(name = "oth.oth.onnba3011.call", description = "Onnba3011 호출 툴") - @ToolHint(categoryKey = "oth") + @ToolHint(categoryKey = "oth", register = true) Object execute(Onnba3011Request req); } From 15bb39bdcbb81343d1927d2f50e79d276eb49113 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:47:10 +0900 Subject: [PATCH 17/28] feat: force all tools to be registered to Redis regardless of register=false --- .../dap/lib/mcp/ToolRegistryHeartbeatSender.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 index 997bb3b5..d168d6fb 100644 --- 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 @@ -92,9 +92,10 @@ public class ToolRegistryHeartbeatSender { ? mcpProperties.getNamespace() + "_" + rawSubToolName : rawSubToolName; - boolean isRegister = hintAnnotation != null && hintAnnotation.register(); - if (!isRegister) { - log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName); + // 요청에 따라 @ToolHint 의 register 값과 상관없이 무조건 등록(true) 처리합니다. + boolean isRegister = true; + if (hintAnnotation != null && !hintAnnotation.register()) { + log.info(" [HeartbeatSender] '{}' 툴은 원래 register=false 이지만 강제로 외부 등록(Redis) 대상에 포함합니다. (최종 이름: {})", baseName, subToolName); } ToolMetadata meta = new ToolMetadata(); From 568b6e1d6376150df023a1d300303f768b309c1b Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:47:34 +0900 Subject: [PATCH 18/28] feat: add smp to pre-initialized category list in DynamicMcpServerManager --- .../io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java index 54cdf044..a07c4163 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/sync/DynamicMcpServerManager.java @@ -55,6 +55,7 @@ public class DynamicMcpServerManager { getOrCreateServer("email"); getOrCreateServer("oth"); getOrCreateServer("sample"); + getOrCreateServer("smp"); // smp 카테고리도 기본적으로 항상 열어두도록 추가 } private McpSyncServer getOrCreateServer(String categoryKey) { From 27129932863af79c0bbdc5d6b241e11b92acf5e4 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 15:58:23 +0900 Subject: [PATCH 19/28] fix: skip domain authorization check for internal system tenant --- .../main/java/io/shinhanlife/dap/mcg/service/ToolPlanner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/service/ToolPlanner.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/service/ToolPlanner.java index f5971eab..96bf1f36 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/service/ToolPlanner.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/service/ToolPlanner.java @@ -81,7 +81,7 @@ public class ToolPlanner { } // 2-1. [신규] 도메인 그룹핑 기반 권한 검증 - if (tenantId != null && toolMetadata.getCategoryKey() != null) { + if (tenantId != null && !tenantId.equalsIgnoreCase("system") && toolMetadata.getCategoryKey() != null) { String normalizedTenantId = tenantId.toLowerCase(); List allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId); From f2e1c8d3884ccfbfad29962cc3fa1625656ca9da Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 16:01:14 +0900 Subject: [PATCH 20/28] fix: correct scaffold package generation --- .../dap/lib/util/ToolScaffolder.java | 85 ++++++++++++++++--- .../dap/lib/util/ToolScaffolderTest.java | 46 +++++++--- 2 files changed, 108 insertions(+), 23 deletions(-) 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 index ac351078..5c1cbb05 100644 --- 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 @@ -127,15 +127,13 @@ public class ToolScaffolder { String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase(); boolean isMci = "MCI".equalsIgnoreCase(routingType); String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase(); - String clientPkgSuffix = ""; String clientPrefixCap = ""; Path mciClientDir = null; if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) { String clientPrefix = clientSystemCode.toLowerCase(); - clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4); clientPrefixCap = toPascalCase(clientSystemCode); - mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4); + mciGroupPath = "infra/itrf/mci/" + clientPrefix; mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath)); } @@ -236,11 +234,11 @@ public class ToolScaffolder { String toolHintLine; if (useSchemaResource) { - toolHintLine = " @ToolHint(register = %s,\n" + - " inputSchemaResource = \"%s\",\n" + - " outputSchemaResource = \"%s\")".formatted(register, inputSchemaClasspath, outputSchemaClasspath); + toolHintLine = (" @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" + + " inputSchemaResource = \"%s\",\n" + + " outputSchemaResource = \"%s\")").formatted(register, group.toLowerCase(Locale.ROOT), interfaceId, inputSchemaClasspath, outputSchemaClasspath); } else { - toolHintLine = " @ToolHint(register = %s)".formatted(register); + toolHintLine = " @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId); } String serviceInterfaceContent = """ @@ -298,7 +296,6 @@ public class ToolScaffolder { import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import java.util.Map; import %s.converter.%sConverter; import %s.%s.io.%s_I; %s @@ -382,8 +379,8 @@ public class ToolScaffolder { ); String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", "."); serviceImplContent = serviceImplContent - .replace("import " + mciIoPackage + "." + interfaceId + "_I;", - "import " + mciIoPackage + "." + interfaceId + "_I;\nimport " + mciIoPackage + "." + interfaceId + "_O;") + .replace("import " + mciIoPackage + ".io." + interfaceId + "_I;", + "import " + mciIoPackage + ".io." + interfaceId + "_I;\nimport " + mciIoPackage + ".io." + interfaceId + "_O;") .replace("Transfer resTransfer", "Transfer<" + interfaceId + "_O> resTransfer") .replace("Object.class", interfaceId + "_O.class") .replace("response.setResultCode(\"SUCCESS\");", @@ -775,6 +772,17 @@ public class ToolScaffolder { log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n"); } + Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json")); + Files.createDirectories(mockResponsePath.getParent()); + Files.writeString(mockResponsePath, mockResponseContent(outputFields)); + + Path generatedTestDir = rootDir.resolve(Paths.get(moduleName, "src/test/java/io/shinhanlife/dap/mcc/biz", group.toLowerCase(), "usecase")); + Files.createDirectories(generatedTestDir); + Path generatedTestPath = generatedTestDir.resolve(baseName + "UseCaseTest.java"); + Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName)); + log.append("[Mock Response] ").append(mockResponsePath).append("\\n"); + log.append("[Unit Test] ").append(generatedTestPath).append("\\n"); + log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n"); log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); return log.toString(); @@ -810,7 +818,7 @@ public class ToolScaffolder { private static String mciIoContent(String packageSuffix, String className, List fields, String author, String createDate) { return """ - package %s.%s; + package %s.%s.io; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -865,6 +873,61 @@ public class ToolScaffolder { }; } + + private static String mockResponseContent(List outputFields) { + StringBuilder json = new StringBuilder("{\n"); + List fields = outputFields == null ? List.of() : outputFields; + boolean first = true; + for (FieldDefinition field : fields) { + if (field.name() == null || field.name().isBlank()) { + continue; + } + if (!first) { + json.append(",\n"); + } + json.append(" \"").append(jsonEscape(field.name())).append("\" : ") + .append(mockValue(field)); + first = false; + } + return json.append("\n}\n").toString(); + } + + private static String mockValue(FieldDefinition field) { + if (field.example() == null || field.example().isBlank()) { + return "null"; + } + return switch (supportedType(field.type())) { + case "Integer", "Long", "Double", "BigDecimal" -> field.example(); + case "Boolean" -> Boolean.parseBoolean(field.example()) ? "true" : "false"; + default -> "\"" + jsonEscape(field.example()) + "\""; + }; + } + + private static String jsonEscape(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } + + private static String useCaseTestContent(String bizPackage, String baseName) { + return """ + package %s.usecase; + + import static org.junit.jupiter.api.Assertions.assertNotNull; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import org.junit.jupiter.api.Test; + + class %sUseCaseTest { + + @Test + void createsToolRequestAndResponseDtos() { + assertNotNull(new %sRequest()); + assertNotNull(new %sResponse()); + } + } + """.formatted(bizPackage, bizPackage, baseName, bizPackage, baseName, + baseName, baseName, baseName); + } private static String toToolName(String moduleName, String group, String baseName) { String moduleDirectory = Path.of(moduleName).getFileName().toString(); String pod = moduleDirectory.startsWith("dap-was-") diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 05434f10..14f396ec 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -15,7 +15,7 @@ class ToolScaffolderTest { Path root; @Test - void generatesManifestReadyToolAndOutputSchemaDto() throws Exception { + void generatesSpringAiToolAndResponseDto() throws Exception { String moduleName = "build/scaffold-manifest-test"; ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName, @@ -26,10 +26,9 @@ class ToolScaffolderTest { String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java")); assertTrue(useCase.contains("name = \"oth.cmm.claim.search\"")); - assertTrue(useCase.contains("version = \"1.0.0\"")); - assertTrue(useCase.contains("timeoutMillis = 300000L")); - assertTrue(response.contains("")); - assertTrue(response.contains("")); + assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")")); + assertTrue(response.contains("private String resultCode;")); + assertTrue(response.contains("private String resultMessage;")); } @Test void usesWasModuleNameAsToolPodPrefix() throws Exception { @@ -56,11 +55,14 @@ class ToolScaffolderTest { Path schemas = root.resolve("dap-was-sample/src/main/resources/tool-schemas/cmm"); assertTrue(Files.exists(schemas.resolve("claim-search-resource-input-schema.json"))); - assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); + assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); String useCase = Files.readString(root.resolve("dap-was-sample/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java")); + assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\",")); + assertTrue(useCase.contains("inputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-input-schema.json\"")); + assertTrue(useCase.contains("outputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-output-schema.json\"")); } @Test - void generatesRequestDtoWithSwaggerSchemaInsteadOfMcpToolParam() throws Exception { + void generatesRequestDtoFromDeclaredFields() throws Exception { String moduleName = root.resolve("dap-was-pay").toString(); ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR 조회", "pay", "MCI", moduleName, @@ -70,8 +72,8 @@ class ToolScaffolderTest { String request = Files.readString(requestPath); assertTrue(request.contains("import io.swagger.v3.oas.annotations.media.Schema;")); - assertTrue(request.contains("@Schema(example = \"01012345678\")")); - assertTrue(request.contains("@Schema(example = \"테스트 메시지입니다.\")")); + assertTrue(request.contains("@Schema(description = \"Search query\", example = \"example\")")); + assertTrue(request.contains("private String query;")); assertFalse(request.contains("McpToolParam")); Path implementationPath = root.resolve("dap-was-pay/src/main/java/io/shinhanlife/dap/mcc/biz/pay/usecase/impl/SearchHrUseCaseImpl.java"); @@ -95,9 +97,12 @@ class ToolScaffolderTest { Path sourceRoot = root.resolve("dap-was-pay/src/main/java/io/shinhanlife/dap/mcc"); String request = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrRequest.java")); String response = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrResponse.java")); - String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_I.java")); - String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_O.java")); - String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); + String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_I.java")); + String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_O.java")); + String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); String useCase = Files.readString(sourceRoot.resolve("biz/pay/usecase/SearchHrUseCase.java")); + String implementation = Files.readString(sourceRoot.resolve("biz/pay/usecase/impl/SearchHrUseCaseImpl.java")); + assertTrue(mciRequest.contains("package io.shinhanlife.dap.mcc.infra.itrf.mci.dfag.io;"), mciRequest); assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"pay\", mappingId = \"SHEARCH_01\")")); + assertTrue(implementation.contains("import io.shinhanlife.dap.mcc.infra.itrf.mci.dfag.io.SHEARCH_01_O;")); assertTrue(request.contains("private String employeeId;")); assertTrue(request.contains("private Integer page;")); @@ -107,4 +112,21 @@ class ToolScaffolderTest { assertTrue(mciResponse.contains("private String employeeName;")); assertTrue(converter.contains("SearchHrResponse toResponse(SHEARCH_01_O mciRes);")); } + @Test + void generatesMockResponseAndUnitTestSkeletonFromOutputFields() throws Exception { + String moduleName = root.resolve("dap-was-oth").toString(); + List outputFields = List.of( + new ToolScaffolder.FieldDefinition("status", "String", "Claim status", "RECEIVED", true)); + + ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName, + "tester", "2026.08.10", true, null, null, null, List.of(), outputFields); + + Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/oth.cmm.claim.search.json"); + Path useCaseTest = root.resolve("dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java"); + + assertTrue(Files.exists(mockResponse)); + assertTrue(Files.readString(mockResponse).contains("\"status\" : \"RECEIVED\"")); + assertTrue(Files.exists(useCaseTest)); + assertTrue(Files.readString(useCaseTest).contains("class ClaimSearchUseCaseTest")); + } } From 4b08021fc64d41777fc1bc68916910eb95c073d8 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 16:05:44 +0900 Subject: [PATCH 21/28] fix: add AUTO-TESTER to tenant-domains in application.yml to allow DeepBuilder tool execution --- dap-gateway/src/main/resources/application.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dap-gateway/src/main/resources/application.yml b/dap-gateway/src/main/resources/application.yml index 0c7afd8e..e468d0d2 100644 --- a/dap-gateway/src/main/resources/application.yml +++ b/dap-gateway/src/main/resources/application.yml @@ -44,6 +44,8 @@ mcp: tenant-domains: TESTER-DEV: ALL system: ALL + AUTO-TESTER: ALL + auto-tester: ALL mybatis: mapper-locations: classpath:mapper/**/*.xml From 40303ee9b8fea434fd1297c5eed1ffe3ebe526f3 Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 10 Aug 2026 17:24:28 +0900 Subject: [PATCH 22/28] feat: add Glow HTTP tool integration sample --- .../http/component/AxhubHttpComponent.java | 124 ++++++++++++++++++ .../http/component/AxhubHttpDomain.java | 16 +++ .../http/component/AxhubHttpProperties.java | 22 ++++ .../glow/communication/ICommunication.java | 6 + .../http/component/GlowHttpComponent.java | 98 ++++++++++++++ .../module/http/dto/HttpBody.java | 5 + .../module/http/dto/HttpHeader.java | 18 +++ .../module/http/dto/HttpTransfer.java | 19 +++ .../component/AxhubHttpComponentTest.java | 43 ++++++ .../biz/smp/dto/SampleHttpStatusResponse.java | 9 ++ .../smp/usecase/SampleHttpStatusUseCase.java | 15 +++ .../impl/SampleHttpStatusUseCaseImpl.java | 21 +++ .../itrf/http/sample/SampleHttpApiClient.java | 18 +++ .../http/sample/SampleHttpApiResponse.java | 5 + .../src/main/resources/application-local.yml | 8 ++ 15 files changed, 427 insertions(+) create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpDomain.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/glow/communication/ICommunication.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/component/GlowHttpComponent.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpBody.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpHeader.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpTransfer.java create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java create mode 100644 dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/SampleHttpStatusResponse.java create mode 100644 dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java create mode 100644 dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/SampleHttpStatusUseCaseImpl.java create mode 100644 dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiClient.java create mode 100644 dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiResponse.java diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java new file mode 100644 index 00000000..37323b7f --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java @@ -0,0 +1,124 @@ +package io.shinhanlife.dap.lib.integration.http.component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.config.GlowCommunicationProperties; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; +import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent; +import io.shinhanlife.glow.communication.module.http.dto.HttpBody; +import io.shinhanlife.glow.communication.module.http.dto.HttpHeader; +import io.shinhanlife.glow.communication.module.http.dto.HttpTransfer; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +/** + * Tool Pod outbound HTTP component modelled after the ShinhanLife HTTP component. + * It resolves an API domain from configuration and delegates the assembled HttpTransfer to Glow. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AxhubHttpComponent { + + private static final String ANONYMOUS_REQUEST = "AXHUB-TOOL"; + + private final GlowHttpComponent http; + private final ObjectMapper json; + private final GlowCommunicationProperties communicationProperties; + private final AxhubHttpProperties properties; + + public R call(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass) { + return call(domain, uri, inputDto, responseBodyClass, 0); + } + + public R call(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass, int timeout) { + if (uri == null || uri.isBlank()) { + throw new IllegalArgumentException("URI is required."); + } + AxhubHttpProperties.ApiDefinition api = resolveApi(domain); + HttpHeader header = createHeader(api, timeout); + String requestUri = joinPath(api.path(), uri); + HttpTransfer request = HttpTransfer.http() + .header(header) + .domain(api.domain()) + .uri(requestUri) + .method(api.method()) + .contentType(MediaType.APPLICATION_JSON) + .responseEntity(responseBodyClass) + .body(inputDto) + .build(); + + log.info("[AxhubHttpComponent] Glow HTTP call. domain={}, method={}, uri={}", + domain.getCode(), api.method(), requestUri); + ResponseEntity response = http.sync(request); + return convertResponse(response.getBody(), responseBodyClass); + } + + /** Convenience method for APIs configured as internal business Pods. */ + public R callBizPod(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass) { + AxhubHttpProperties.ApiDefinition api = resolveApi(domain); + if (!api.bizPod()) { + throw new IllegalArgumentException("Configured API is not a business Pod: " + domain.getCode()); + } + return call(domain, uri, inputDto, responseBodyClass); + } + + private AxhubHttpProperties.ApiDefinition resolveApi(AxhubHttpDomain domain) { + return properties.getApiList().stream() + .filter(api -> domain.getCode().equals(api.name())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No HTTP API configuration for domain: " + domain.getCode())); + } + + private HttpHeader createHeader(AxhubHttpProperties.ApiDefinition api, int timeout) { + HttpHeader header = new HttpHeader(); + if (api.bizPod()) { + header.set("X-POD-TO-POD", "true"); + } + McpRequestHeaders inbound = McpRequestHeaderContext.current(); + if (inbound == null) { + header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST); + } else { + putIfPresent(header, "trace-id", inbound.traceId()); + putIfPresent(header, "request-id", inbound.requestId()); + putIfPresent(header, "X-USER-ID", inbound.encryptedEmployeeId()); + } + header.setReadTimeout(timeout == 0 ? defaultReadTimeout() : timeout); + return header; + } + + private int defaultReadTimeout() { + return communicationProperties == null || communicationProperties.getHttp() == null + ? 0 : communicationProperties.getHttp().getReadTimeout(); + } + + private R convertResponse(HttpBody responseBody, Class responseBodyClass) { + String content = responseBody == null ? null : responseBody.content(); + if (responseBodyClass == String.class) { + return responseBodyClass.cast(content); + } + try { + return json.readValue(content, responseBodyClass); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Failed to convert HTTP response to " + responseBodyClass.getSimpleName(), e); + } + } + + private void putIfPresent(HttpHeader header, String name, String value) { + if (value != null && !value.isBlank()) { + header.set(name, value); + } + } + + private String joinPath(String basePath, String uri) { + String left = basePath == null ? "" : basePath.replaceAll("/+$", ""); + String right = uri.startsWith("/") ? uri : "/" + uri; + return left + right; + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpDomain.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpDomain.java new file mode 100644 index 00000000..4dc24c18 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpDomain.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.lib.integration.http.component; + +/** Registered outbound HTTP API domains. Add a domain only after its endpoint is configured. */ +public enum AxhubHttpDomain { + SAMPLE("sample"); + + private final String code; + + AxhubHttpDomain(String code) { + this.code = code; + } + + public String getCode() { + return code; + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java new file mode 100644 index 00000000..8237898c --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java @@ -0,0 +1,22 @@ +package io.shinhanlife.dap.lib.integration.http.component; + +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Component; + +/** Domain-to-endpoint configuration for outbound Tool HTTP calls. */ +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "axhub.http") +public class AxhubHttpProperties { + + private List apiList = new ArrayList<>(); + + public record ApiDefinition(String name, String domain, String path, HttpMethod method, boolean bizPod) { + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/ICommunication.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/ICommunication.java new file mode 100644 index 00000000..f93b1ea2 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/ICommunication.java @@ -0,0 +1,6 @@ +package io.shinhanlife.glow.communication; + +/** Minimal Glow communication contract used by the temporary compatibility layer. */ +public interface ICommunication { + O sync(I request); +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/component/GlowHttpComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/component/GlowHttpComponent.java new file mode 100644 index 00000000..34265031 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/component/GlowHttpComponent.java @@ -0,0 +1,98 @@ +package io.shinhanlife.glow.communication.module.http.component; + +import io.shinhanlife.glow.communication.ICommunication; +import io.shinhanlife.glow.communication.module.http.dto.HttpBody; +import io.shinhanlife.glow.communication.module.http.dto.HttpHeader; +import io.shinhanlife.glow.communication.module.http.dto.HttpTransfer; +import java.util.List; +import java.util.Map; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClient; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * Temporary compatibility implementation of the internal Glow HTTP component. + * Replace this class with the official Glow HTTP JAR when it is supplied. + */ +@Component +public class GlowHttpComponent implements ICommunication, ResponseEntity> { + + private final RestClient restClient; + + public GlowHttpComponent(RestClient.Builder restClientBuilder) { + this.restClient = restClientBuilder.build(); + } + + @Override + public ResponseEntity sync(HttpTransfer request) { + if (request == null || request.getHeader() == null || request.getMethod() == null + || !StringUtils.hasText(request.getDomain())) { + throw new IllegalArgumentException("Glow HTTP request header, domain, and method are required."); + } + if (HttpMethod.GET.equals(request.getMethod())) { + return get(request); + } + if (HttpMethod.POST.equals(request.getMethod())) { + return post(request); + } + if (HttpMethod.PUT.equals(request.getMethod())) { + return put(request); + } + if (HttpMethod.DELETE.equals(request.getMethod())) { + return delete(request); + } + throw new IllegalArgumentException("Unsupported HTTP method: " + request.getMethod()); + } + + private ResponseEntity get(HttpTransfer request) { + return toHttpBody(restClient.get().uri(buildUri(request, true)) + .headers(headers -> applyHeaders(headers, request.getHeader())) + .retrieve().toEntity(String.class)); + } + + private ResponseEntity post(HttpTransfer request) { + return toHttpBody(restClient.post().uri(buildUri(request, false)) + .contentType(contentType(request)).headers(headers -> applyHeaders(headers, request.getHeader())) + .body(request.getBody()).retrieve().toEntity(String.class)); + } + + private ResponseEntity put(HttpTransfer request) { + return toHttpBody(restClient.put().uri(buildUri(request, false)) + .contentType(contentType(request)).headers(headers -> applyHeaders(headers, request.getHeader())) + .body(request.getBody()).retrieve().toEntity(String.class)); + } + + private ResponseEntity delete(HttpTransfer request) { + return toHttpBody(restClient.delete().uri(buildUri(request, true)) + .headers(headers -> applyHeaders(headers, request.getHeader())) + .retrieve().toEntity(String.class)); + } + + private String buildUri(HttpTransfer request, boolean includeQueryParameters) { + String uri = request.getDomain() + (request.getUri() == null ? "" : request.getUri()); + if (!includeQueryParameters || !(request.getBody() instanceof Map parameters)) { + return uri; + } + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uri); + parameters.forEach((key, value) -> { if (key != null && value != null) builder.queryParam(String.valueOf(key), value); }); + return builder.build().encode().toUriString(); + } + + private MediaType contentType(HttpTransfer request) { + return request.getContentType() == null ? MediaType.APPLICATION_JSON : request.getContentType(); + } + + private void applyHeaders(HttpHeaders target, HttpHeader source) { + target.setAccept(List.of(MediaType.APPLICATION_JSON)); + source.getValues().forEach((name, value) -> { if (StringUtils.hasText(name) && StringUtils.hasText(value)) target.set(name, value); }); + } + + private ResponseEntity toHttpBody(ResponseEntity response) { + return new ResponseEntity<>(new HttpBody(response.getBody()), response.getHeaders(), response.getStatusCode()); + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpBody.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpBody.java new file mode 100644 index 00000000..2406e7b5 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpBody.java @@ -0,0 +1,5 @@ +package io.shinhanlife.glow.communication.module.http.dto; + +/** Raw body returned by the Glow HTTP transport. */ +public record HttpBody(String content) { +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpHeader.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpHeader.java new file mode 100644 index 00000000..396f4a7d --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpHeader.java @@ -0,0 +1,18 @@ +package io.shinhanlife.glow.communication.module.http.dto; + +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.Getter; +import lombok.Setter; + +/** Glow HTTP request headers and timeout metadata. */ +@Getter +@Setter +public class HttpHeader { + private Map values = new LinkedHashMap<>(); + private int readTimeout; + + public void set(String name, String value) { + values.put(name, value); + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpTransfer.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpTransfer.java new file mode 100644 index 00000000..30fb28e9 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/http/dto/HttpTransfer.java @@ -0,0 +1,19 @@ +package io.shinhanlife.glow.communication.module.http.dto; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; + +/** Glow HTTP request envelope. Use HttpTransfer.http() to build a request. */ +@Getter +@Builder(builderMethodName = "http") +public class HttpTransfer { + private final HttpHeader header; + private final String domain; + private final String uri; + private final HttpMethod method; + private final MediaType contentType; + private final Class responseEntity; + private final T body; +} \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java new file mode 100644 index 00000000..edb6c79f --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java @@ -0,0 +1,43 @@ +package io.shinhanlife.dap.lib.integration.http.component; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.config.GlowCommunicationProperties; +import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; + +class AxhubHttpComponentTest { + + @Test + void callResolvesDomainBuildsGlowTransferAndDeserializesJsonResponse() { + RestClient.Builder builder = RestClient.builder(); + MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build(); + GlowHttpComponent glowHttpComponent = new GlowHttpComponent(builder); + AxhubHttpProperties properties = new AxhubHttpProperties(); + properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition( + "sample", "https://api.example.test", "/v1", HttpMethod.GET, false))); + AxhubHttpComponent component = new AxhubHttpComponent( + glowHttpComponent, new ObjectMapper(), new GlowCommunicationProperties(), properties); + + server.expect(requestTo("https://api.example.test/v1/status")) + .andExpect(header("X-ANONYMOUS-REQ", "AXHUB-TOOL")) + .andRespond(withSuccess("{\"status\":\"OK\"}", APPLICATION_JSON)); + + SampleResponse response = component.call(AxhubHttpDomain.SAMPLE, "/status", null, SampleResponse.class); + + assertThat(response.status()).isEqualTo("OK"); + server.verify(); + } + + record SampleResponse(String status) { + } +} \ No newline at end of file diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/SampleHttpStatusResponse.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/SampleHttpStatusResponse.java new file mode 100644 index 00000000..c39aaa86 --- /dev/null +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/SampleHttpStatusResponse.java @@ -0,0 +1,9 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Tool response for the HTTP integration sample. */ +public record SampleHttpStatusResponse( + @Schema(description = "External API processing status", example = "OK") String status, + @Schema(description = "External API response message", example = "Sample API is available") String message) { +} \ No newline at end of file diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java new file mode 100644 index 00000000..cb74912b --- /dev/null +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java @@ -0,0 +1,15 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.mcc.biz.smp.dto.SampleHttpStatusResponse; +import org.springaicommunity.mcp.annotation.McpTool; + +/** Sample Tool that demonstrates a configured HTTP API integration. */ +public interface SampleHttpStatusUseCase { + + @McpTool(name = "oth.smp.sample.http.status", + title = "Sample external HTTP API status", + description = "Calls the configured sample HTTP API and returns its status.") + @ToolHint(register = false, categoryKey = "smp", mappingId = "HTTP_SAMPLE_001") + SampleHttpStatusResponse execute(); +} \ No newline at end of file diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/SampleHttpStatusUseCaseImpl.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/SampleHttpStatusUseCaseImpl.java new file mode 100644 index 00000000..0ddee3f3 --- /dev/null +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/SampleHttpStatusUseCaseImpl.java @@ -0,0 +1,21 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.smp.dto.SampleHttpStatusResponse; +import io.shinhanlife.dap.mcc.biz.smp.usecase.SampleHttpStatusUseCase; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.SampleHttpApiClient; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.SampleHttpApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SampleHttpStatusUseCaseImpl implements SampleHttpStatusUseCase { + + private final SampleHttpApiClient sampleHttpApiClient; + + @Override + public SampleHttpStatusResponse execute() { + SampleHttpApiResponse response = sampleHttpApiClient.getStatus(); + return new SampleHttpStatusResponse(response.status(), response.message()); + } +} \ No newline at end of file diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiClient.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiClient.java new file mode 100644 index 00000000..22767044 --- /dev/null +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiClient.java @@ -0,0 +1,18 @@ +package io.shinhanlife.dap.mcc.infra.itrf.http.sample; + +import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent; +import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpDomain; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** Example of a Tool-specific HTTP client using the configured AX HUB HTTP domain. */ +@Component +@RequiredArgsConstructor +public class SampleHttpApiClient { + + private final AxhubHttpComponent http; + + public SampleHttpApiResponse getStatus() { + return http.call(AxhubHttpDomain.SAMPLE, "/status", null, SampleHttpApiResponse.class); + } +} \ No newline at end of file diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiResponse.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiResponse.java new file mode 100644 index 00000000..9eb93085 --- /dev/null +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleHttpApiResponse.java @@ -0,0 +1,5 @@ +package io.shinhanlife.dap.mcc.infra.itrf.http.sample; + +/** DTO matching the external API JSON response: { "status": "OK", "message": "..." }. */ +public record SampleHttpApiResponse(String status, String message) { +} \ No newline at end of file diff --git a/dap-was-oth/src/main/resources/application-local.yml b/dap-was-oth/src/main/resources/application-local.yml index 1e73a06d..b0854d89 100644 --- a/dap-was-oth/src/main/resources/application-local.yml +++ b/dap-was-oth/src/main/resources/application-local.yml @@ -43,6 +43,14 @@ axhub: url: http://localhost:8081 tool: url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} + # Registered outbound HTTP APIs. Tool code selects the domain name, never a free-form URL. + http: + api-list: + - name: sample + domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8099} + path: "" + method: GET + biz-pod: false sol: req-detail: From 4275f76104443bf3dce6e062cdf0f85a6e04a102 Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 11 Aug 2026 10:18:36 +0900 Subject: [PATCH 23/28] feat: enforce underscore MCP tool names --- README.md | 10 +-- .../lib/config/AxhubHttpConfiguration.java | 21 ++++++ .../dap/lib/util/ToolScaffolder.java | 2 +- .../lib/validation/McpToolNameValidator.java | 4 +- .../config/AxhubHttpConfigurationTest.java | 31 +++++++++ .../lib/mcp/McpToolMethodRegistryTest.java | 8 +-- .../dap/lib/util/ToolScaffolderTest.java | 6 +- .../dap/lib/util/ToolSchemaResolverTest.java | 4 +- .../dap/lib/util/ToolSourceUpdaterTest.java | 6 +- .../validation/McpToolNameValidatorTest.java | 40 ++++++++--- .../mcc/manifest/ToolManifestServiceTest.java | 2 +- .../mcc/biz/cmm/usecase/BalanceUseCase.java | 2 +- .../cmm/usecase/BillingProcessUseCase.java | 2 +- .../mcc/biz/cmm/usecase/BondIssueUseCase.java | 2 +- .../ClaimSearchSchemaSampleUseCase.java | 2 +- .../biz/cmm/usecase/CommonUtilityUseCase.java | 4 +- .../cmm/usecase/ContractInquiryUseCase.java | 2 +- .../biz/cmm/usecase/CustomerInfoUseCase.java | 2 +- .../cmm/usecase/MetaCommonCodeUseCase.java | 2 +- .../mcc/biz/cmm/usecase/MetaTableUseCase.java | 2 +- .../cmm/usecase/TemplateUtilityUseCase.java | 2 +- .../mcc/biz/oth/usecase/Onnba3011UseCase.java | 2 +- .../smp/usecase/DailyQuoteToolUseCase.java | 2 +- .../smp/usecase/ExchangeRateToolUseCase.java | 2 +- .../smp/usecase/SampleHttpStatusUseCase.java | 2 +- .../biz/smp/usecase/TeamMemberUseCase.java | 2 +- .../biz/smp/usecase/WeatherToolUseCase.java | 2 +- .../biz/sol/usecase/SolReqDetailUseCase.java | 2 +- .../biz/sol/usecase/SolReqListUseCase.java | 2 +- .../cmm/converter/ClaimSearchConverter.java | 14 ++++ .../mcc/biz/cmm/dto/ClaimSearchRequest.java | 16 +++++ .../mcc/biz/cmm/dto/ClaimSearchResponse.java | 22 ++++++ .../biz/cmm/usecase/ClaimSearchUseCase.java | 29 ++++++++ .../usecase/impl/ClaimSearchUseCaseImpl.java | 68 +++++++++++++++++++ .../mcc/biz/sms/usecase/SmsToolUseCase.java | 2 +- .../infra/itrf/mci/ncla/MciNclaClient.java | 30 ++++++++ .../infra/itrf/mci/ncla/io/CLCNNB00001_I.java | 14 ++++ .../infra/itrf/mci/ncla/io/CLCNNB00001_O.java | 17 +++++ .../mock-responses/sms_cmm_claim_search.json | 5 ++ .../claim-search-resource-input-schema.json | 11 +++ .../claim-search-resource-output-schema.json | 16 +++++ .../cmm/usecase/ClaimSearchUseCaseTest.java | 16 +++++ 42 files changed, 380 insertions(+), 52 deletions(-) create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/AxhubHttpConfigurationTest.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/ClaimSearchConverter.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/MciNclaClient.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_I.java create mode 100644 dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_O.java create mode 100644 dap-was-sms/src/main/resources/mock-responses/sms_cmm_claim_search.json create mode 100644 dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json create mode 100644 dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json create mode 100644 dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java diff --git a/README.md b/README.md index 1b485501..3bed7c26 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ UI에서 사용하는 Tailwind CSS와 Chart.js는 `dap-gateway/src/main/resource "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "oth.cmm.claim.search", + "name": "oth_cmm_claim_search", "arguments": { "claimNo": "CLM2026070100120" } @@ -170,8 +170,8 @@ Agent나 외부 클라이언트의 표준 MCP 진입은 Gateway를 사용합니 Tool 함수명은 아래 4단계 규칙을 사용합니다. ```text -pod.domain.service.action -예: oth.cmm.claim.search +pod_domain_service_action +예: oth_cmm_claim_search ``` - `pod`: Tool Pod 식별자 (`oth`, `sms` 등) @@ -234,7 +234,7 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv 배포 전에는 다음을 확인합니다. -- Tool 이름의 전역 중복 여부와 `pod.domain.service.action` 규칙(아래 확인 사항 반영 후) +- Tool 이름의 전역 중복 여부와 `pod_domain_service_action` 규칙(아래 확인 사항 반영 후) - Request/Response Schema 및 실제 예제 JSON - Tool Pod 단위 테스트와 Gateway 경유 호출 - MCI/EAI 오류 코드의 사용자용 응답 매핑 @@ -266,6 +266,6 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니다. - `validateMcpToolNames`는 `dap-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다. -- Tool Scaffold는 `dap-was-sms`처럼 선택한 Pod 이름을 Tool 함수명 첫 번째 구간에 반영합니다. 예: `sms.cmm.notification.send` +- Tool Scaffold는 `dap-was-sms`처럼 선택한 Pod 이름을 Tool 함수명 첫 번째 구간에 반영합니다. 예: `sms_cmm_notification_send` - Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다. - Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다. \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java new file mode 100644 index 00000000..d124b376 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java @@ -0,0 +1,21 @@ +package io.shinhanlife.dap.lib.config; + +import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +/** + * Registers the temporary Glow HTTP compatibility component from the DAP library scan scope. + * The bean is only created when an official GlowHttpComponent has not already been supplied. + */ +@Configuration(proxyBeanMethods = false) +public class AxhubHttpConfiguration { + + @Bean + @ConditionalOnMissingBean(GlowHttpComponent.class) + public GlowHttpComponent glowHttpComponent(RestClient.Builder restClientBuilder) { + return new GlowHttpComponent(restClientBuilder); + } +} \ No newline at end of file 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 index 5c1cbb05..c73861ab 100644 --- 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 @@ -940,7 +940,7 @@ public class ToolScaffolder { String[] words = normalizedName.split("\\s+"); String service = words[0]; String action = words.length == 1 ? "execute" : words[words.length - 1]; - return "%s.%s.%s.%s".formatted( + return "%s_%s_%s_%s".formatted( pod.toLowerCase(Locale.ROOT), group.toLowerCase(Locale.ROOT), service, 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 89aea24d..d662c925 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 @@ -28,7 +28,7 @@ import java.util.regex.Pattern; public final class McpToolNameValidator { private static final Pattern TOOL_NAME_PATTERN = Pattern.compile("\\bname\\s*=\\s*\\\"([^\\\"]+)\\\""); - private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$"); + private static final Pattern TOOL_NAME_CONVENTION = Pattern.compile("^[a-zA-Z0-9_-]{1,128}$"); private McpToolNameValidator() { } @@ -139,7 +139,7 @@ public final class McpToolNameValidator { } private static String buildInvalidNameMessage(List>> invalidNames) { - StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected pod.domain.service.action using lowercase letters, digits, or hyphens."); + StringBuilder message = new StringBuilder("Invalid MCP tool name(s): expected 1-128 characters using letters, digits, underscores, or hyphens."); for (Map.Entry> invalidName : invalidNames) { message.append("\n\n").append(invalidName.getKey()); invalidName.getValue().stream() diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/AxhubHttpConfigurationTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/AxhubHttpConfigurationTest.java new file mode 100644 index 00000000..4e7f10f5 --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/AxhubHttpConfigurationTest.java @@ -0,0 +1,31 @@ +package io.shinhanlife.dap.lib.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.client.RestClient; + +class AxhubHttpConfigurationTest { + + @Test + void registersGlowHttpComponentFromDapLibConfiguration() { + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class)) { + assertThat(context.getBean(GlowHttpComponent.class)).isNotNull(); + } + } + + @Configuration + @Import(AxhubHttpConfiguration.class) + static class TestConfiguration { + + @Bean + RestClient.Builder restClientBuilder() { + return RestClient.builder(); + } + } +} \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java index 050d6b10..37b321be 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java @@ -18,10 +18,10 @@ class McpToolMethodRegistryTest { registry.initialize(); - McpToolMethodRegistry.RegisteredTool tool = registry.find("oth.cmm.echo.search"); + McpToolMethodRegistry.RegisteredTool tool = registry.find("oth_cmm_echo_search"); assertNotNull(tool); assertEquals("execute", tool.method().getName()); - assertEquals("oth.cmm.echo.search", tool.annotation().name()); + assertEquals("oth_cmm_echo_search", tool.annotation().name()); } @Test @@ -42,14 +42,14 @@ class McpToolMethodRegistryTest { } static class EchoTool { - @McpTool(name = "oth.cmm.echo.search") + @McpTool(name = "oth_cmm_echo_search") public String execute(String request) { return request; } } static class DuplicateEchoTool { - @McpTool(name = "oth.cmm.echo.search") + @McpTool(name = "oth_cmm_echo_search") public String execute(String request) { return request; } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 14f396ec..9e83b09f 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -25,7 +25,7 @@ class ToolScaffolderTest { String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java")); String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java")); - assertTrue(useCase.contains("name = \"oth.cmm.claim.search\"")); + assertTrue(useCase.contains("name = \"oth_cmm_claim_search\"")); assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")")); assertTrue(response.contains("private String resultCode;")); assertTrue(response.contains("private String resultMessage;")); @@ -41,7 +41,7 @@ class ToolScaffolderTest { "src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/NotificationSendUseCase.java"); String useCase = Files.readString(useCasePath); - assertTrue(useCase.contains("name = \"sms.cmm.notification.send\"")); + assertTrue(useCase.contains("name = \"sms_cmm_notification_send\"")); } @Test @@ -121,7 +121,7 @@ class ToolScaffolderTest { ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName, "tester", "2026.08.10", true, null, null, null, List.of(), outputFields); - Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/oth.cmm.claim.search.json"); + Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/oth_cmm_claim_search.json"); Path useCaseTest = root.resolve("dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java"); assertTrue(Files.exists(mockResponse)); diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java index 00a72f94..9e0cab83 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java @@ -57,13 +57,13 @@ class ToolSchemaResolverTest { } static class AutomaticSchemaTool { - @McpTool(name = "oth.test.automatic.search") + @McpTool(name = "oth_test_automatic_search") void search(AutomaticRequest request) { } } static class AutomaticOutputSchemaTool { - @McpTool(name = "oth.test.output.search") + @McpTool(name = "oth_test_output_search") SimpleResponse search(AutomaticRequest request) { return null; } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java index 4c0bd514..7c89445b 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java @@ -21,16 +21,16 @@ class ToolSourceUpdaterTest { import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; interface SampleUseCase { - @McpTool(name = "oth.cmm.sample.search", description = "old") + @McpTool(name = "oth_cmm_sample_search", description = "old") @ToolHint(register = false, requiresApproval = false) void search(); } """); - ToolSourceUpdater.updateToolSource(temporaryRoot, "oth.cmm.sample.search", "customer", "new", true, true); + ToolSourceUpdater.updateToolSource(temporaryRoot, "oth_cmm_sample_search", "customer", "new", true, true); String updated = Files.readString(source); - assertTrue(updated.contains("@McpTool(name = \"oth.cmm.sample.search\", description = \"new\")")); + assertTrue(updated.contains("@McpTool(name = \"oth_cmm_sample_search\", description = \"new\")")); assertTrue(updated.contains("@ToolHint(register = true, requiresApproval = true")); assertTrue(updated.contains("categoryKey = \"customer\""), updated); } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java index 81ec6e16..2bb4b6ba 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java @@ -31,35 +31,53 @@ class McpToolNameValidatorTest { @Test void rejectsDuplicateMcpToolNamesAcrossToolModules() throws IOException { - writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send"); - writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send"); + writeToolSource("dap-was-first", "FirstTool.java", "first", "oth_sms_notification_send"); + writeToolSource("dap-was-second", "SecondTool.java", "second", "oth_sms_notification_send"); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> McpToolNameValidator.assertUnique(temporaryRoot)); - assertTrue(exception.getMessage().contains("oth.sms.notification.send")); + assertTrue(exception.getMessage().contains("oth_sms_notification_send")); assertTrue(exception.getMessage().contains("dap-was-first")); assertTrue(exception.getMessage().contains("dap-was-second")); } @Test void validationRunnerRejectsDuplicateMcpToolNamesBeforePackaging() throws IOException { - writeToolSource("dap-was-first", "FirstTool.java", "first", "oth.sms.notification.send"); - writeToolSource("dap-was-second", "SecondTool.java", "second", "oth.sms.notification.send"); + writeToolSource("dap-was-first", "FirstTool.java", "first", "oth_sms_notification_send"); + writeToolSource("dap-was-second", "SecondTool.java", "second", "oth_sms_notification_send"); assertThrows(IllegalStateException.class, () -> McpToolNameValidationRunner.validate(temporaryRoot)); } @Test - void rejectsToolNameOutsidePodDomainServiceActionConvention() throws IOException { - writeToolSource("dap-was-first", "FirstTool.java", "first", "bond_issue"); + void rejectsToolNameOutsideConfiguredPattern() throws IOException { + writeToolSource("dap-was-first", "FirstTool.java", "first", "bond.issue"); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> McpToolNameValidator.assertUnique(temporaryRoot)); assertTrue(exception.getMessage().contains("Invalid MCP tool name(s)")); - assertTrue(exception.getMessage().contains("bond_issue")); + assertTrue(exception.getMessage().contains("bond.issue")); + } + @Test + void acceptsLettersDigitsUnderscoresAndDashesWithin128Characters() throws IOException { + String validName = "Tool_Name-" + "a".repeat(118); + writeToolSource("dap-was-first", "FirstTool.java", "first", validName); + + assertDoesNotThrow(() -> McpToolNameValidator.assertUnique(temporaryRoot)); + } + + @Test + void rejectsToolNameLongerThan128Characters() throws IOException { + String invalidName = "a".repeat(129); + writeToolSource("dap-was-first", "FirstTool.java", "first", invalidName); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> McpToolNameValidator.assertUnique(temporaryRoot)); + + assertTrue(exception.getMessage().contains(invalidName)); } @Test void acceptsCurrentProjectToolNames() { @@ -71,11 +89,11 @@ class McpToolNameValidatorTest { Path root = findProjectRoot(); assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java", - "oth.cmm.customer.detail", "detail"); + "oth_cmm_customer_detail", "detail"); assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java", - "oth.cmm.billing.process", "process"); + "oth_cmm_billing_process", "process"); assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java", - "oth.cmm.bond.issue", "issue"); + "oth_cmm_bond_issue", "issue"); } private void assertToolName(Path root, String relativePath, String expectedName, String legacyName) throws IOException { diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java index 6efc38ec..cd29bc3a 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java @@ -52,7 +52,7 @@ class ToolManifestServiceTest { void rejectsEntireManifestWhenToolNameDoesNotMatchConfiguredPrefix() { McpProperties properties = manifestProperties("insurance-processing", "processing."); ToolManifestService service = new ToolManifestService( - () -> List.of(tool("notification.sms.send", "1.0.0", 3000)), objectMapper, properties); + () -> List.of(tool("notification_sms_send", "1.0.0", 3000)), objectMapper, properties); IllegalStateException error = assertThrows(IllegalStateException.class, service::currentManifest); diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java index 8e200f8c..85f00573 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface BalanceUseCase { - @McpTool(name = "oth.cmm.balance.inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.") + @McpTool(name = "oth_cmm_balance_inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.") @ToolHint(register = false, categoryKey = "cmm", mappingId = "ACC_001") Object execute(BalanceRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java index 8c856e1d..96270e8e 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java @@ -7,7 +7,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface BillingProcessUseCase { Object getStatus(BillingStatusRequest req); - @McpTool(name = "oth.cmm.billing.process", title = "청구 프로세스 툴", description = "청구 처리") + @McpTool(name = "oth_cmm_billing_process", title = "청구 프로세스 툴", description = "청구 처리") @ToolHint(register = false, categoryKey = "cmm", mappingId = "BILL_002") Object processBilling(BillingProcessRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java index 840bd479..eabfcfdc 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface BondIssueUseCase { Object check(BondCheckRequest req); - @McpTool(name = "oth.cmm.bond.issue", title = "채권 발행 툴", description = "증권 발행 테스트1") + @McpTool(name = "oth_cmm_bond_issue", title = "채권 발행 툴", description = "증권 발행 테스트1") @ToolHint(register = false, categoryKey = "cmm", mappingId = "BOND_002") Object issue(BondIssueRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java index 7eb8bfde..13c4a7de 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; public interface ClaimSearchSchemaSampleUseCase { - @McpTool(name = "oth.cmm.claim.search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true)) + @McpTool(name = "oth_cmm_claim_search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true)) @ToolHint(register = false, categoryKey = "cmm", inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json") diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java index 74f85e62..47c0a56f 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java @@ -8,11 +8,11 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface CommonUtilityUseCase { Object registerVacation(VacationRegisterRequest req); - @McpTool(name = "oth.cmm.leave.count", title = "공통 유틸리티 툴", description = "연차 갯수 조회") + @McpTool(name = "oth_cmm_leave_count", title = "공통 유틸리티 툴", description = "연차 갯수 조회") @ToolHint(register = false, categoryKey = "cmm", mappingId = "HR_VAC_02") Object getLeaveCount(LeaveCountRequest data); - @McpTool(name = "oth.cmm.secret.execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트") + @McpTool(name = "oth_cmm_secret_execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트") @ToolHint(register = false, categoryKey = "cmm", mappingId = "SECRET_001") Object secretTool(LeaveCountRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java index 6e21fcb4..3031622e 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface ContractInquiryUseCase { Object getStatus(ContractStatusRequest req); - @McpTool(name = "oth.cmm.contract.detail", title = "계약 상세조회 툴", description = "계약상세 조회") + @McpTool(name = "oth_cmm_contract_detail", title = "계약 상세조회 툴", description = "계약상세 조회") @ToolHint(register = false, categoryKey = "cmm", mappingId = "CNTR_002") Object getDetail(ContractDetailRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java index c70d5116..30409a3b 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface CustomerInfoUseCase { Object getGrade(CustomerGradeRequest req); - @McpTool(name = "oth.cmm.customer.detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회") + @McpTool(name = "oth_cmm_customer_detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회") @ToolHint(register = false, categoryKey = "cmm", mappingId = "CRM_002") Object getDetail(CustomerDetailRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java index 3829fe0a..a7b2ae08 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java @@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest; * */ public interface MetaCommonCodeUseCase { - @McpTool(name = "oth.cmm.common-code.lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "oth_cmm_commonCode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001") Object execute(MetaCommonCodeRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java index 03cc0dc1..9c809945 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java @@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest; * */ public interface MetaTableUseCase { - @McpTool(name = "oth.cmm.meta.table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "oth_cmm_meta_table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001") Object execute(MetaTableRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java index 3b9d4da7..64e0b7e1 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; import java.util.Map; public interface TemplateUtilityUseCase { - @McpTool(name = "oth.cmm.template.url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.") + @McpTool(name = "oth_cmm_template_url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.") @ToolHint(categoryKey = "cmm") Map getTemplateFileUrl(TemplateDownloadRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java index 7f8b3654..6af755c0 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java @@ -5,7 +5,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.oth.dto.*; public interface Onnba3011UseCase { - @McpTool(name = "oth.oth.onnba3011.call", description = "Onnba3011 호출 툴") + @McpTool(name = "oth_oth_onnba3011_call", description = "Onnba3011 호출 툴") @ToolHint(categoryKey = "oth", register = true) Object execute(Onnba3011Request req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java index ade68558..9ecc6c1a 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java @@ -7,7 +7,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest; import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse; public interface DailyQuoteToolUseCase { - @McpTool(name = "oth.smp.quote.daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.") + @McpTool(name = "oth_smp_quote_daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.") @ToolHint(register = false, categoryKey = "smp", mappingId = "QUOTE_001") DailyQuoteResponse execute(DailyQuoteRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java index 77cd0ab8..bf7c1972 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java @@ -9,7 +9,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest; import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse; public interface ExchangeRateToolUseCase { - @McpTool(name = "oth.smp.exchange-rate.inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)") + @McpTool(name = "oth_smp_exchangeRate_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)") @ToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001") ExchangeRateResponse execute(ExchangeRateRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java index cb74912b..1eb10081 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java @@ -7,7 +7,7 @@ import org.springaicommunity.mcp.annotation.McpTool; /** Sample Tool that demonstrates a configured HTTP API integration. */ public interface SampleHttpStatusUseCase { - @McpTool(name = "oth.smp.sample.http.status", + @McpTool(name = "oth_smp_sample_status", title = "Sample external HTTP API status", description = "Calls the configured sample HTTP API and returns its status.") @ToolHint(register = false, categoryKey = "smp", mappingId = "HTTP_SAMPLE_001") diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java index a87cdb19..dd801934 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest; public interface TeamMemberUseCase { - @McpTool(name = "oth.smp.team.list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "oth_smp_team_list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "smp", mappingId = "DIRECT0001") Object execute(TeamMemberRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java index d46d76ca..d60b1e10 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.*; public interface WeatherToolUseCase { - @McpTool(name = "oth.smp.weather.inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.") + @McpTool(name = "oth_smp_weather_inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.") @ToolHint(register = false, categoryKey = "smp", mappingId = "WEATHER_001") WeatherResponse execute(WeatherRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java index 3817d65e..853fff5f 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java @@ -21,7 +21,7 @@ import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; */ public interface SolReqDetailUseCase { - @McpTool(name = "oth.sol.request.detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true)) + @McpTool(name = "oth_sol_request_detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000002") Object execute(SolReqDetailRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java index 2b14abe4..a3c6e2a0 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; public interface SolReqListUseCase { - @McpTool(name = "oth.sol.request.list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "oth_sol_request_list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000001") Object execute(SolReqListRequest req); } diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/ClaimSearchConverter.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/ClaimSearchConverter.java new file mode 100644 index 00000000..ed5d79c8 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/converter/ClaimSearchConverter.java @@ -0,0 +1,14 @@ +package io.shinhanlife.dap.mcc.biz.cmm.converter; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_O; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface ClaimSearchConverter { + CLCNNB00001_I toLegacyRequest(ClaimSearchRequest request); + ClaimSearchRequest toRequest(CLCNNB00001_I mciRequest); + ClaimSearchResponse toResponse(CLCNNB00001_O mciRes); +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java new file mode 100644 index 00000000..644d3146 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequest.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ClaimSearchRequest { + @Schema(description = "보험금 청구번호", example = "CLM202608100001", requiredMode = Schema.RequiredMode.REQUIRED) + private String claimNo; + + @Schema(description = "보험 계약번호", example = "10023456789") + private String contractNo; + +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java new file mode 100644 index 00000000..301d3f8a --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java @@ -0,0 +1,22 @@ +package io.shinhanlife.dap.mcc.biz.cmm.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ClaimSearchResponse { + private String resultCode; + + private String resultMessage; + @Schema(description = "청구 처리 상태 코드", example = "RECEIVED", requiredMode = Schema.RequiredMode.REQUIRED) + private String status; + + @Schema(description = "청구 처리 상태명", example = "접수", requiredMode = Schema.RequiredMode.REQUIRED) + private String statusLabel; + + @Schema(description = "승인 금액", example = "150000") + private Long approvedAmount; + +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java new file mode 100644 index 00000000..500cc752 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java @@ -0,0 +1,29 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import org.springaicommunity.mcp.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.usecase + * @className ClaimSearchUseCase + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.08.10 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.08.10  jade    최초생성
+ *
+ * 
+ */ +public interface ClaimSearchUseCase { + + @McpTool(name = "sms_cmm_claim_search", title = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.", description = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.") + @ToolHint(register = false, categoryKey = "cmm", mappingId = "CLCNNB00001", + inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", + outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json") + ClaimSearchResponse execute(ClaimSearchRequest req); +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java new file mode 100644 index 00000000..8ce78e7b --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java @@ -0,0 +1,68 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; +import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchUseCase; +import io.shinhanlife.glow.communication.dto.Transfer; +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import io.shinhanlife.dap.mcc.biz.cmm.converter.ClaimSearchConverter; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_I; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_O; +import io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.MciNclaClient; + + +/** + * @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl + * @className ClaimSearchUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.08.10 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.08.10  jade    최초생성
+ *
+ * 
+ */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ClaimSearchUseCaseImpl implements ClaimSearchUseCase { + + private final MciNclaClient mci; + private final ClaimSearchConverter converter; + + @Override + public ClaimSearchResponse execute(ClaimSearchRequest req) { + log.info("[MCI Tool] {} 요청 수신.", "sms_cmm_claim_search"); + try { + // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) + CLCNNB00001_I mciReq = converter.toLegacyRequest(req); + + Transfer resTransfer = mci.callTo( + "CLCNNB00001", + null, + mciReq, + CLCNNB00001_O.class + ); + ClaimSearchResponse response = new ClaimSearchResponse(); + if (resTransfer.getBody() != null) { + response = converter.toResponse(resTransfer.getBody()); + } + response.setResultCode("SUCCESS"); + response.setResultMessage(resTransfer.getBody() != null + ? "MCI call completed." + : "MCI call completed without a response body."); + return response; + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + ClaimSearchResponse response = new ClaimSearchResponse(); + response.setResultCode("ERROR"); + response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error"); + return response; + } + } +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java index 9f80d439..ccfdf333 100644 --- a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java @@ -5,7 +5,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sms.dto.*; public interface SmsToolUseCase { - @McpTool(name = "sms.sms.msg.send", title = "SMS 발송 툴", description = "SMS 발송 기능을 제공합니다.") + @McpTool(name = "sms_sms_msg_send", title = "SMS 발송 툴", description = "SMS 발송 기능을 제공합니다.") @ToolHint(register = false, categoryKey = "sms", mappingId = "SMS_SEND") Object sendSms(SmsSendRequest req); } diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/MciNclaClient.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/MciNclaClient.java new file mode 100644 index 00000000..5c81c4fa --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/MciNclaClient.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla; + +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 io.shinhanlife.dap.mcc.infra.itrf.mci.ncla + * @className MciNclaClient + * @description AX HUB 시스템 처리 클래스 + * @author jade + * @create 2026.08.10 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.08.10  jade    최초생성
+ *
+ * 
+ */ +@Component +@RequiredArgsConstructor +public class MciNclaClient { + private final AxhubMciComponent mci; + + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + return mci.callTo(interfaceId, dummy, mciReq, resType); + } +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_I.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_I.java new file mode 100644 index 00000000..1c5a292f --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_I.java @@ -0,0 +1,14 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class CLCNNB00001_I { + @Schema(description = "보험금 청구번호", example = "CLM202608100001", requiredMode = Schema.RequiredMode.REQUIRED) + private String claimNo; + + @Schema(description = "보험 계약번호", example = "10023456789") + private String contractNo; + +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_O.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_O.java new file mode 100644 index 00000000..99760997 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/mci/ncla/io/CLCNNB00001_O.java @@ -0,0 +1,17 @@ +package io.shinhanlife.dap.mcc.infra.itrf.mci.ncla.io; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class CLCNNB00001_O { + @Schema(description = "청구 처리 상태 코드", example = "RECEIVED", requiredMode = Schema.RequiredMode.REQUIRED) + private String status; + + @Schema(description = "청구 처리 상태명", example = "접수", requiredMode = Schema.RequiredMode.REQUIRED) + private String statusLabel; + + @Schema(description = "승인 금액", example = "150000") + private Long approvedAmount; + +} diff --git a/dap-was-sms/src/main/resources/mock-responses/sms_cmm_claim_search.json b/dap-was-sms/src/main/resources/mock-responses/sms_cmm_claim_search.json new file mode 100644 index 00000000..9c4f9c03 --- /dev/null +++ b/dap-was-sms/src/main/resources/mock-responses/sms_cmm_claim_search.json @@ -0,0 +1,5 @@ +{ + "status" : "RECEIVED", + "statusLabel" : "접수", + "approvedAmount" : 150000 +} diff --git a/dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json b/dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json new file mode 100644 index 00000000..e6f63087 --- /dev/null +++ b/dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "TODO_FIELD": { + "type": "string", + "description": "TODO: 파라미터 설명을 입력하세요." + } + }, + "required": [] +} diff --git a/dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json b/dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json new file mode 100644 index 00000000..9c280db8 --- /dev/null +++ b/dap-was-sms/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "description": "처리 결과 상태 (SUCCESS / FAILURE)", + "enum": ["SUCCESS", "FAILURE"] + }, + "message": { + "type": "string", + "description": "처리 결과 메시지" + } + }, + "required": ["status"] +} diff --git a/dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java b/dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java new file mode 100644 index 00000000..37356b3e --- /dev/null +++ b/dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.cmm.usecase; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest; +import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; +import org.junit.jupiter.api.Test; + +class ClaimSearchUseCaseTest { + + @Test + void createsToolRequestAndResponseDtos() { + assertNotNull(new ClaimSearchRequest()); + assertNotNull(new ClaimSearchResponse()); + } +} From 041f939c334cca7356074fcc6840ec34df86c0f2 Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 11 Aug 2026 10:28:46 +0900 Subject: [PATCH 24/28] fix: honor tool registration flag --- .../lib/mcp/ToolRegistryHeartbeatSender.java | 12 ++-- .../dap/lib/util/ToolScaffolder.java | 2 +- .../mcp/ToolRegistryHeartbeatSenderTest.java | 56 +++++++++++++++++++ .../mcc/biz/oth/usecase/Onnba3011UseCase.java | 2 +- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSenderTest.java 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 index d168d6fb..3744cb07 100644 --- 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 @@ -91,11 +91,13 @@ public class ToolRegistryHeartbeatSender { String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() ? mcpProperties.getNamespace() + "_" + rawSubToolName : rawSubToolName; - - // 요청에 따라 @ToolHint 의 register 값과 상관없이 무조건 등록(true) 처리합니다. - boolean isRegister = true; - if (hintAnnotation != null && !hintAnnotation.register()) { - log.info(" [HeartbeatSender] '{}' 툴은 원래 register=false 이지만 강제로 외부 등록(Redis) 대상에 포함합니다. (최종 이름: {})", baseName, subToolName); + // @ToolHint(register = false)인 Tool은 메타데이터 조회에는 남기되, + // Gateway 등록 및 heartbeat 전송 대상에서는 제외합니다. + // ToolHint가 없는 기존 Tool은 이전 동작과 동일하게 등록합니다. + boolean isRegister = hintAnnotation == null || hintAnnotation.register(); + if (!isRegister) { + log.info(" [HeartbeatSender] '{}' Tool is excluded from Gateway registration because register=false. (tool name: {})", + baseName, subToolName); } ToolMetadata meta = new ToolMetadata(); 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 index c73861ab..c60409ae 100644 --- 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 @@ -81,7 +81,7 @@ public class ToolScaffolder { String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null; - String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, inputSchemaResource, outputSchemaResource); + String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource); System.out.println(result); } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSenderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSenderTest.java new file mode 100644 index 00000000..9fb55a49 --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSenderTest.java @@ -0,0 +1,56 @@ +package io.shinhanlife.dap.lib.mcp; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.springaicommunity.mcp.annotation.McpTool; +import org.springframework.context.ApplicationContext; + +class ToolRegistryHeartbeatSenderTest { + + @Test + void excludesRegisterFalseToolFromGatewayRegistrationTargets() throws Exception { + ApplicationContext applicationContext = mock(ApplicationContext.class); + when(applicationContext.getBeansOfType(Object.class)).thenReturn(Map.of("disabledTool", new DisabledTool())); + ToolRegistryHeartbeatSender sender = new ToolRegistryHeartbeatSender( + applicationContext, new ObjectMapper(), new McpProperties(), mock(ToolSchemaResolver.class)); + + setField(sender, "podUrl", "http://localhost:8084"); + + sender.init(); + + assertThat(sender.getAllScannedTools()).singleElement() + .extracting(tool -> tool.getIsRegistered()) + .isEqualTo(false); + assertThat(registeredTools(sender)).isEmpty(); + } + + private void setField(Object target, String name, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } + @SuppressWarnings("unchecked") + private List registeredTools(ToolRegistryHeartbeatSender sender) throws Exception { + Field field = ToolRegistryHeartbeatSender.class.getDeclaredField("registeredTools"); + field.setAccessible(true); + return (List) field.get(sender); + } + + static class DisabledTool { + + @McpTool(name = "test_disabled_tool") + @ToolHint(register = false) + void execute() { + } + } +} \ No newline at end of file diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java index 6af755c0..be27dcaf 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java @@ -6,6 +6,6 @@ import io.shinhanlife.dap.mcc.biz.oth.dto.*; public interface Onnba3011UseCase { @McpTool(name = "oth_oth_onnba3011_call", description = "Onnba3011 호출 툴") - @ToolHint(categoryKey = "oth", register = true) + @ToolHint(categoryKey = "oth", register = false) Object execute(Onnba3011Request req); } From 8312eee1d10e9b0ed35eb800f64fd454f37503bd Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 11 Aug 2026 13:22:48 +0900 Subject: [PATCH 25/28] feat: scaffold Glow HTTP tool integration --- .../http/component/AxhubHttpProperties.java | 2 +- .../dap/lib/util/ToolScaffolder.java | 118 ++++++++++++++++-- .../main/resources/glow/application-glow.yml | 7 ++ .../dap/lib/util/ToolScaffolderTest.java | 35 ++++++ .../src/main/resources/application-local.yml | 8 -- 5 files changed, 153 insertions(+), 17 deletions(-) diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java index 8237898c..f183fa05 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java @@ -12,7 +12,7 @@ import org.springframework.stereotype.Component; @Getter @Setter @Component -@ConfigurationProperties(prefix = "axhub.http") +@ConfigurationProperties(prefix = "glow.communication.http") public class AxhubHttpProperties { private List apiList = new ArrayList<>(); 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 index c60409ae..19d7ec8d 100644 --- 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 @@ -7,8 +7,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.Scanner; /** @@ -193,7 +195,7 @@ public class ToolScaffolder { .replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "") .replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;") .replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;"); - reqContent = dtoContent(bizPackage, baseName + "Request", inputFields, author, createDate, true); + reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true); Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); // Generate Response DTO @@ -227,7 +229,7 @@ public class ToolScaffolder { // TODO: Add response fields here. Do not include PII in the Tool response. } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); - resContent = dtoContent(bizPackage, baseName + "Response", outputFields, author, createDate, false); + resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false); Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent); String toolName = toToolName(moduleName, group, baseName); @@ -386,7 +388,9 @@ public class ToolScaffolder { .replace("response.setResultCode(\"SUCCESS\");", "if (resTransfer.getBody() != null) {\n response = converter.toResponse(resTransfer.getBody());\n }\n response.setResultCode(\"SUCCESS\");"); } else { - serviceImplContent = """ + serviceImplContent = "HTTP".equalsIgnoreCase(routingType) + ? httpUseCaseImplContent(bizPackage, baseName, interfaceId, author, createDate) + : """ package %s.usecase.impl; import %s.dto.%sRequest; @@ -420,9 +424,16 @@ public class ToolScaffolder { private final %sConverter converter; @Override - public Object execute(%sRequest req) { + public %sResponse execute(%sRequest req) { // %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); - return executeLegacy("%s", "%s", req); // Or pass legacyRequest + Object legacyResponse = executeLegacy("%s", "%s", req); // Or pass legacyRequest + if (legacyResponse instanceof %sResponse response) { + return response; + } + %sResponse response = new %sResponse(); + response.setResultCode("SUCCESS"); + response.setResultMessage("Legacy call completed."); + return response; } } """.formatted( @@ -439,6 +450,10 @@ public class ToolScaffolder { baseName, baseName, baseName, + baseName, + baseName, + baseName, + baseName, routingType, interfaceId ); } @@ -644,6 +659,7 @@ public class ToolScaffolder { private String content; } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + legacyReqContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyRequest", inputFields, author, createDate, true); Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent); String legacyResContent = """ @@ -670,6 +686,7 @@ public class ToolScaffolder { // TODO: Add legacy response fields here } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + legacyResContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyResponse", outputFields, author, createDate, true); Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent); String converterContent = """ @@ -719,6 +736,7 @@ public class ToolScaffolder { bizPackage, baseName, author, createDate, createDate, author, baseName, baseName, baseName, baseName, baseName, baseName, baseName ); + converterContent = legacyConverterContent(bizPackage, baseName); Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); log.append("\n=========================================\n"); @@ -797,7 +815,7 @@ public class ToolScaffolder { private static String dtoContent(String packageName, String className, List fields, String author, String createDate, boolean request) { - String body = fieldLines(fields); + String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage")); if (!request) { body = " private String resultCode;\n\n private String resultMessage;\n" + body; } @@ -829,6 +847,57 @@ public class ToolScaffolder { """.formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields)); } + private static String httpUseCaseImplContent(String bizPackage, String baseName, String interfaceId, String author, String createDate) { + return """ + package %s.usecase.impl; + + import %s.converter.%sConverter; + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.legacy.%sLegacyRequest; + import %s.legacy.%sLegacyResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent; + import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpDomain; + import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; + + /** + * HTTP Tool implementation. Calls the Glow HTTP adapter through AxhubHttpComponent. + * Configure the target domain in AxhubHttpDomain and glow.communication.http.api-list before use. + */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase { + + private final %sConverter converter; + private final AxhubHttpComponent http; + + @Override + public %sResponse execute(%sRequest req) { + %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); + %sLegacyResponse legacyResponse = http.call( + AxhubHttpDomain.SAMPLE, + "/%s", + legacyRequest, + %sLegacyResponse.class + ); + + %sResponse response = converter.toResponse(legacyResponse); + response.setResultCode("SUCCESS"); + response.setResultMessage("HTTP API call completed."); + return response; + } + } + """.formatted( + bizPackage, bizPackage, baseName, bizPackage, baseName, bizPackage, baseName, + bizPackage, baseName, bizPackage, baseName, bizPackage, baseName, + baseName, baseName, baseName, baseName, baseName, baseName, baseName, + interfaceId, baseName, baseName); + } private static String mciConverterContent(String bizPackage, String baseName, String mciPackage, String interfaceId) { return """ package %s.converter; @@ -850,9 +919,43 @@ public class ToolScaffolder { baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId); } + private static String legacyConverterContent(String bizPackage, String baseName) { + return """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.legacy.%sLegacyRequest; + import %s.legacy.%sLegacyResponse; + import org.mapstruct.Mapper; + import org.mapstruct.ReportingPolicy; + + @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) + public interface %sConverter { + %sLegacyRequest toLegacyRequest(%sRequest request); + %sRequest toRequest(%sLegacyRequest legacyRequest); + %sResponse toResponse(%sLegacyResponse legacyResponse); + } + """.formatted( + bizPackage, bizPackage, baseName, bizPackage, baseName, + bizPackage, baseName, bizPackage, baseName, + baseName, baseName, baseName, baseName, baseName, baseName, baseName); + } private static String fieldLines(List fields) { + return fieldLines(fields, Set.of()); + } + + private static String fieldLines(List fields, Set excludedNames) { StringBuilder source = new StringBuilder(); + Set generatedNames = new LinkedHashSet<>(); for (FieldDefinition field : fields == null ? List.of() : fields) { + if (field == null || field.name() == null || field.name().isBlank()) { + continue; + } + String fieldName = field.name().trim(); + if (excludedNames.contains(fieldName) || !generatedNames.add(fieldName)) { + continue; + } String type = supportedType(field.type()); String description = field.description() == null ? "" : field.description().replace("\"", "\\\""); String example = field.example() == null ? "" : field.example().replace("\"", "\\\""); @@ -861,11 +964,10 @@ public class ToolScaffolder { if (field.required()) { source.append(", requiredMode = Schema.RequiredMode.REQUIRED"); } - source.append(")\n private ").append(type).append(' ').append(field.name()).append(";\n\n"); + source.append(")\n private ").append(type).append(' ').append(fieldName).append(";\n\n"); } return source.toString(); } - private static String supportedType(String type) { return switch (type == null ? "String" : type) { case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; 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 e73d3223..e5925405 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow.yml @@ -21,6 +21,13 @@ glow: http: connection-timeout: 5 read-timeout: 5 + # HTTP Tool target catalog. Replace or add entries after the business endpoint is agreed. + api-list: + - name: sample + domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8099} + path: "" + method: GET + biz-pod: false mci: uri: /ntl_mci/dap_rcv receive-uri: /itrf/mciReceive diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 9e83b09f..5989594a 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -129,4 +129,39 @@ class ToolScaffolderTest { assertTrue(Files.exists(useCaseTest)); assertTrue(Files.readString(useCaseTest).contains("class ClaimSearchUseCaseTest")); } + + @Test + void generatesDtoPackageAndRemovesDuplicateResponseFields() throws Exception { + String moduleName = root.resolve("dap-was-http").toString(); + List outputFields = List.of( + new ToolScaffolder.FieldDefinition("resultCode", "String", "API result", "SUCCESS", true), + new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong Gildong", false), + new ToolScaffolder.FieldDefinition("employeeName", "String", "Duplicate name", "Duplicate", false)); + + List inputFields = List.of( + new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true)); + ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "Employee search", "smp", "HTTP", moduleName, + "tester", "2026.08.11", false, null, null, null, inputFields, outputFields); + + Path dtoRoot = root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto"); + String request = Files.readString(dtoRoot.resolve("EmployeeSearchRequest.java")); + String response = Files.readString(dtoRoot.resolve("EmployeeSearchResponse.java")); + + assertTrue(request.contains("package io.shinhanlife.dap.mcc.biz.smp.dto;"), request); + assertTrue(response.contains("package io.shinhanlife.dap.mcc.biz.smp.dto;"), response); + assertTrue(response.indexOf("private String resultCode;") == response.lastIndexOf("private String resultCode;"), response); + assertTrue(response.indexOf("private String employeeName;") == response.lastIndexOf("private String employeeName;"), response); + String converter = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/converter/EmployeeSearchConverter.java")); + String legacyRequest = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/legacy/EmployeeSearchLegacyRequest.java")); + assertFalse(converter.contains("phoneNumber"), converter); + assertTrue(converter.contains("unmappedTargetPolicy = ReportingPolicy.IGNORE"), converter); + assertTrue(legacyRequest.contains("private String employeeId;"), legacyRequest); + String implementation = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java")); + assertTrue(implementation.contains("public EmployeeSearchResponse execute(EmployeeSearchRequest req)"), implementation); + assertTrue(implementation.contains("import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent;"), implementation); + assertTrue(implementation.contains("private final AxhubHttpComponent http;"), implementation); + assertTrue(implementation.contains("AxhubHttpDomain.SAMPLE,"), implementation); + assertTrue(implementation.contains("\"/HR_EMPLOYEE_SEARCH\""), implementation); + assertFalse(implementation.contains("executeLegacy(\"HTTP\""), implementation); + } } diff --git a/dap-was-oth/src/main/resources/application-local.yml b/dap-was-oth/src/main/resources/application-local.yml index b0854d89..1e73a06d 100644 --- a/dap-was-oth/src/main/resources/application-local.yml +++ b/dap-was-oth/src/main/resources/application-local.yml @@ -43,14 +43,6 @@ axhub: url: http://localhost:8081 tool: url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} - # Registered outbound HTTP APIs. Tool code selects the domain name, never a free-form URL. - http: - api-list: - - name: sample - domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8099} - path: "" - method: GET - biz-pod: false sol: req-detail: From d3ceec615f160571b36c7fbc69b834da3e4a9157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=ED=9D=AC=EC=A4=80?= Date: Tue, 11 Aug 2026 14:57:18 +0900 Subject: [PATCH 26/28] ci: update docker post-deploy cleanup to prune build cache and unused tagged images --- .gitea/workflows/deploy.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b6a561cf..e37bf65a 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -61,7 +61,8 @@ jobs: docker system prune -f ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle was-sms was-oth - # 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 자동 소거 청소! - docker image prune -f + # 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소! + docker image prune -a -f + docker builder prune -a -f echo "CI/CD Deploy Success!" From 68608569ce37fee3e0333583ccd3e00bd0611770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=ED=9D=AC=EC=A4=80?= Date: Tue, 11 Aug 2026 15:05:19 +0900 Subject: [PATCH 27/28] ci: exclude bin and act_runner_data from rsync deletion --- .gitea/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index e37bf65a..9ad99713 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -28,7 +28,7 @@ jobs: run: | echo "Copying latest code to /app (Host Volume) and removing stale files..." if command -v rsync &> /dev/null; then - rsync -a --delete --exclude='.git' . /app/ + rsync -a --delete --exclude='.git' --exclude='bin' --exclude='act_runner_data' . /app/ else rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle cp -a . /app/ From e250b1149c4c4cf97c4d82bb3363afc77b003861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=ED=9D=AC=EC=A4=80?= Date: Tue, 11 Aug 2026 16:16:33 +0900 Subject: [PATCH 28/28] ci: update gitea runner registration token --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b241d65f..73e48c60 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,7 +105,7 @@ services: image: gitea/act_runner:latest environment: - GITEA_INSTANCE_URL=https://git.devjun.net - - GITEA_RUNNER_REGISTRATION_TOKEN=2N7ciuFIp6Jm29ewtzfl9ruQtpwGgB4ngFcXC2lF + - GITEA_RUNNER_REGISTRATION_TOKEN=BruvOCsirwaflqpu3kUMl95OfX8bMH39LN077jg8 - GITEA_RUNNER_NAME=ociwp-runner - GLOW_COMMUNICATION_EXTMCI_PORT=8080 - GITEA_RUNNER_LABELS=ubuntu-latest,ubuntu-22.04,linux-arm64