diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index b6a561cf..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/ @@ -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!" 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 diff --git a/README.md b/README.md index 5306e961..3bed7c26 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` | 로컬 환경에서는 민감한 값은 환경 변수로만 주입합니다. @@ -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/build.gradle b/build.gradle index 5f062bcd..2cd07775 100644 --- a/build.gradle +++ b/build.gradle @@ -1,60 +1,88 @@ +// 루트 Gradle 설정: 모든 Gateway/Tool Pod 모듈이 공유하는 빌드 기준을 정의합니다. plugins { + // Java 컴파일, 테스트, JAR 생성 기능을 제공합니다. id 'java' - id 'org.springframework.boot' version '4.0.5' apply false + + // 하위 실행 모듈에서 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:2.0.0" + + // 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 87507fb1..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:2.0.0' - // 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-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..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,9 +93,9 @@ 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)) + .model(selectedModel).build()) .stream() .content(); 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/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/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); 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..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)"); } @@ -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); 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..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) { @@ -68,7 +69,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(); 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-gateway/src/main/resources/application.yml b/dap-gateway/src/main/resources/application.yml index 3cb3e3a5..e468d0d2 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: @@ -43,6 +44,8 @@ mcp: tenant-domains: TESTER-DEV: ALL system: ALL + AUTO-TESTER: ALL + auto-tester: ALL mybatis: mapper-locations: classpath:mapper/**/*.xml 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-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-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 diff --git a/dap-was-lib/build.gradle b/dap-was-lib/build.gradle index 2c803d59..9a2774c0 100644 --- a/dap-was-lib/build.gradle +++ b/dap-was-lib/build.gradle @@ -1,40 +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:3.3.0' - 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/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/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/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/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/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..f183fa05 --- /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 = "glow.communication.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/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..6b0c6cfb --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java @@ -0,0 +1,101 @@ +package io.shinhanlife.dap.lib.mcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.Error; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** Executes a cached Tool independently from its HTTP or MCP transport. */ +@Slf4j +@Service +@RequiredArgsConstructor +public class McpToolExecutionService { + + private final McpToolMethodRegistry toolMethodRegistry; + private final ObjectMapper objectMapper; + private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; + private final ToolSchemaResolver toolSchemaResolver; + + public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders, + Map arguments) { + String headerRequestId = requestHeaders == null ? null : requestHeaders.headerRequestId(); + String traceId = requestHeaders == null ? null : requestHeaders.traceId(); + String requestId = requestHeaders == null ? null : requestHeaders.requestId(); + log.info("[Tool] IN - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName); + + McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName); + if (resolvedTool == null) { + return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, headerRequestId); + } + ToolExecutionResult validationFailure = validateInput(resolvedTool, arguments, headerRequestId); + if (validationFailure != null) { + return validationFailure; + } + try { + Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments)); + ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, headerRequestId); + if (outputFailure != null) { + return outputFailure; + } + Map headers = new HashMap<>(); + if (traceId != null) headers.put("trace-id", traceId); + if (requestId != null) headers.put("request-id", requestId); + log.info("[Tool] OUT - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName); + return new ToolExecutionResult(200, methodResult, headers); + } catch (Exception error) { + log.error("[Tool] Tool execution failed. tool={}", functionName, error); + return error(502, "TOOL_ERROR", "Tool execution failed", headerRequestId); + } + } + + private ToolExecutionResult validateInput(McpToolMethodRegistry.RegisteredTool tool, + Map 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(McpToolMethodRegistry.RegisteredTool tool, Object argument) throws Exception { + return tool.method().getParameterCount() == 0 ? tool.method().invoke(tool.bean()) : tool.method().invoke(tool.bean(), argument); + } + + private ToolExecutionResult validateOutput(McpToolMethodRegistry.RegisteredTool tool, + Object methodResult, String requestId) { + try { + Map 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()); + } +} \ 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/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 d9f23f01..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,33 +5,29 @@ import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.mcc.presentation.BusinessToolController; -import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; -import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext; -import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; /** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */ @Component -@ConditionalOnBean(BusinessToolController.class) +@ConditionalOnBean(McpToolExecutionService.class) public class ToolPodMcpToolSynchronizer { private final McpSyncServer mcpServer; private final ToolRegistryHeartbeatSender heartbeatSender; - private final BusinessToolController businessToolController; + private final McpToolExecutionService toolExecutionService; private final ObjectMapper objectMapper; public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender, - BusinessToolController businessToolController, ObjectMapper objectMapper) { + McpToolExecutionService toolExecutionService, ObjectMapper objectMapper) { this.mcpServer = mcpServer; this.heartbeatSender = heartbeatSender; - this.businessToolController = businessToolController; + this.toolExecutionService = toolExecutionService; this.objectMapper = objectMapper; } @@ -46,12 +42,14 @@ public class ToolPodMcpToolSynchronizer { McpSchema.Tool mcpTool = McpSchema.Tool.builder() .name(tool.getName()) .description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription()) - .inputSchema(tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema()) - .annotations(McpSchema.ToolAnnotations.builder() - .readOnlyHint(Boolean.TRUE.equals(tool.getReadOnlyHint())) - .destructiveHint(Boolean.TRUE.equals(tool.getDestructiveHint())) - .idempotentHint(Boolean.TRUE.equals(tool.getIdempotentHint())) - .openWorldHint(Boolean.TRUE.equals(tool.getOpenWorldHint())).build()) + .inputSchema(toJsonSchema(tool.getParametersSchema())) + .annotations(new McpSchema.ToolAnnotations( + tool.getDisplayName(), + tool.getReadOnlyHint(), + tool.getDestructiveHint(), + tool.getIdempotentHint(), + tool.getOpenWorldHint(), + null)) .build(); return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool) .callHandler((context, request) -> invoke(tool.getName(), McpRequestHeaderContext.current(), request.arguments())).build(); @@ -59,15 +57,9 @@ public class ToolPodMcpToolSynchronizer { private McpSchema.CallToolResult invoke(String toolName, McpRequestHeaders requestHeaders, Map arguments) { - ResponseEntity response = businessToolController.executeDynamicTool( - toolName, - requestHeaders == null ? null : requestHeaders.headerRequestId(), - requestHeaders == null ? null : requestHeaders.traceId(), - requestHeaders == null ? null : requestHeaders.requestId(), - requestHeaders == null ? null : requestHeaders.encryptedEmployeeId(), - arguments); - boolean failed = !response.getStatusCode().is2xxSuccessful(); - Object body = response.getBody(); + ToolExecutionResult result = toolExecutionService.execute(toolName, requestHeaders, arguments); + boolean failed = !result.isSuccess(); + Object body = result.body(); try { return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body)) .structuredContent(body).isError(failed).build(); @@ -83,4 +75,19 @@ public class ToolPodMcpToolSynchronizer { schema.put("additionalProperties", false); return schema; } + @SuppressWarnings("unchecked") + private McpSchema.JsonSchema toJsonSchema(Map 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()); + } } 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..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 @@ -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; @@ -44,14 +44,13 @@ import org.springframework.util.ClassUtils; import org.springframework.web.client.RestClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import io.shinhanlife.dap.mcc.presentation.BusinessToolController; @Slf4j @Component @Configuration @EnableScheduling @RequiredArgsConstructor -@ConditionalOnBean(BusinessToolController.class) +@ConditionalOnBean(McpToolExecutionService.class) public class ToolRegistryHeartbeatSender { private final ApplicationContext applicationContext; @@ -92,10 +91,13 @@ public class ToolRegistryHeartbeatSender { String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() ? mcpProperties.getNamespace() + "_" + rawSubToolName : rawSubToolName; - - boolean isRegister = hintAnnotation != null && hintAnnotation.register(); + // @ToolHint(register = false)인 Tool은 메타데이터 조회에는 남기되, + // Gateway 등록 및 heartbeat 전송 대상에서는 제외합니다. + // ToolHint가 없는 기존 Tool은 이전 동작과 동일하게 등록합니다. + boolean isRegister = hintAnnotation == null || hintAnnotation.register(); if (!isRegister) { - log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName); + log.info(" [HeartbeatSender] '{}' Tool is excluded from Gateway registration because register=false. (tool name: {})", + baseName, subToolName); } ToolMetadata meta = new ToolMetadata(); 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..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 @@ -2,7 +2,7 @@ package io.shinhanlife.dap.lib.util; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import org.springframework.ai.mcp.annotation.McpToolParam; +import org.springaicommunity.mcp.annotation.McpToolParam; import io.swagger.v3.oas.annotations.media.Schema; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; @@ -76,7 +76,8 @@ public class JsonSchemaGenerator { if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) { fieldSchema.put("description", schemaAnnotation.description()); } - if (schemaAnnotation.required() && !requiredList.contains(field.getName())) { + if ((schemaAnnotation.required() || schemaAnnotation.requiredMode() == Schema.RequiredMode.REQUIRED) + && !requiredList.contains(field.getName())) { requiredList.add(field.getName()); } if (!schemaAnnotation.pattern().isEmpty()) { 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..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 @@ -46,7 +46,11 @@ public class PodScaffolder { public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException { String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - + return scaffoldPod(rootDir, moduleName, portStr, shortName, author, createDate); + } + + static String scaffoldPod(Path rootDir, String moduleName, String portStr, String shortName, + String author, String createDate) throws IOException { Path modulePath = rootDir.resolve(Paths.get(moduleName)); if (Files.exists(modulePath)) { return "[오류] 이미 존재하는 모듈입니다: " + moduleName; @@ -59,25 +63,27 @@ public class PodScaffolder { log.append("[2/6] build.gradle 생성 중...\n"); String buildGradle = """ plugins { + // Spring Boot 3.5.11 version is managed by the root build.gradle. id 'org.springframework.boot' } + dependencies { + // Shared MCP, Glow integration, validation, logging, Lombok and MapStruct configuration. implementation project(':dap-was-lib') } - dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' - } """; Files.writeString(modulePath.resolve("build.gradle"), buildGradle); log.append("[3/6] Dockerfile 생성 중...\n"); String dockerfile = """ - FROM eclipse-temurin:21-jdk-alpine + FROM eclipse-temurin:21-jre-alpine WORKDIR /app - COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar + RUN apk add --no-cache tzdata + ENV TZ=Asia/Seoul + COPY %s/build/libs/*-SNAPSHOT.jar app.jar + EXPOSE %s ENTRYPOINT ["java", "-jar", "app.jar"] - """.formatted(moduleName); + """.formatted(moduleName, portStr); Files.writeString(modulePath.resolve("Dockerfile"), dockerfile); log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n"); @@ -132,15 +138,24 @@ public class PodScaffolder { org.apache.kafka: ERROR mcp: namespace: "" + manifest: + bundle-id: %s + name-prefix: "" security: tenant-domains: TESTER-DEV: ALL - """.formatted(portStr, moduleName); + """.formatted(portStr, moduleName, moduleName.replace("dap-", "")); Files.writeString(resPath.resolve("application.yml"), applicationYml); String applicationLocalYml = """ # Local 환경 전용 설정 (H2 메모리 DB 등) spring: + config: + activate: + on-profile: local + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-local.yml datasource: url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; driverClassName: com.p6spy.engine.spy.P6SpyDriver @@ -186,6 +201,14 @@ public class PodScaffolder { server: port: ${PORT:%s} + spring: + config: + activate: + on-profile: dev + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-dev.yml + axhub: gateway: url: https://axhubmcp.devjun.net @@ -223,6 +246,46 @@ public class PodScaffolder { """.formatted(portStr, portStr); Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml); + String applicationTestYml = """ + server: + port: ${PORT:%s} + + spring: + config: + activate: + on-profile: test + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-test.yml + + axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} + """.formatted(portStr); + Files.writeString(resPath.resolve("application-test.yml"), applicationTestYml); + + String applicationProdYml = """ + server: + port: ${PORT:%s} + + spring: + config: + activate: + on-profile: prod + import: + - classpath:glow/application-glow.yml + - classpath:glow/application-glow-prod.yml + + axhub: + gateway: + url: ${AXHUB_GATEWAY_URL} + tool: + url: ${AXHUB_TOOL_URL} + """.formatted(portStr); + Files.writeString(resPath.resolve("application-prod.yml"), applicationProdYml); + String logbackXml = """ 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..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 @@ -1,14 +1,16 @@ package io.shinhanlife.dap.lib.util; -import org.springframework.ai.mcp.annotation.McpToolParam; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.Scanner; /** @@ -42,6 +44,9 @@ public class ToolScaffolder { private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc"; private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc"; + public record FieldDefinition(String name, String type, String description, String example, boolean required) { + } + public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); @@ -74,10 +79,11 @@ public class ToolScaffolder { String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): "); boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim()); - String inputSchemaResource = useSchemaResource ? "classpath:mcp/schema/" + toKebabCase(baseName) + "-resource-input-schema.json" : null; - String outputSchemaResource = useSchemaResource ? "classpath:mcp/schema/" + toKebabCase(baseName) + "-resource-output-schema.json" : null; + String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/"; + String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; + String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null; - String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, inputSchemaResource, outputSchemaResource); + String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource); System.out.println(result); } @@ -94,6 +100,12 @@ public class ToolScaffolder { } public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource) throws IOException { + return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, + register, clientSystemCode, inputSchemaResource, outputSchemaResource, + List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of()); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List inputFields, List outputFields) throws IOException { baseName = toPascalCase(baseName); String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); @@ -110,22 +122,20 @@ public class ToolScaffolder { String schemaBaseName = toKebabCase(baseName); String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json"; String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json"; - Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mcp/schema")); - String inputSchemaClasspath = "classpath:mcp/schema/" + inputSchemaFileName; - String outputSchemaClasspath = "classpath:mcp/schema/" + outputSchemaFileName; + Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/tool-schemas", group.toLowerCase())); + String inputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + inputSchemaFileName; + String outputSchemaClasspath = "classpath:tool-schemas/" + group.toLowerCase() + "/" + outputSchemaFileName; String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase(); boolean isMci = "MCI".equalsIgnoreCase(routingType); String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase(); - String clientPkgSuffix = ""; String clientPrefixCap = ""; Path mciClientDir = null; if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) { String clientPrefix = clientSystemCode.toLowerCase(); - clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4); clientPrefixCap = toPascalCase(clientSystemCode); - mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4); + mciGroupPath = "infra/itrf/mci/" + clientPrefix; mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath)); } @@ -178,6 +188,14 @@ public class ToolScaffolder { private String message; } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + reqContent = reqContent + .replace("import com.fasterxml.jackson.annotation.JsonInclude;", + "import com.fasterxml.jackson.annotation.JsonInclude;\nimport io.swagger.v3.oas.annotations.media.Schema;") + .replaceAll("(?m)^\\s*@McpToolParam\\([^\\r\\n]*\\)\\R", "") + .replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "") + .replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;") + .replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;"); + reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true); Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); // Generate Response DTO @@ -211,23 +229,24 @@ public class ToolScaffolder { // TODO: Add response fields here. Do not include PII in the Tool response. } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false); Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent); String toolName = toToolName(moduleName, group, baseName); String toolHintLine; if (useSchemaResource) { - toolHintLine = " @ToolHint(register = %s,\n" + - " inputSchemaResource = \"%s\",\n" + - " outputSchemaResource = \"%s\")".formatted(register, inputSchemaClasspath, outputSchemaClasspath); + toolHintLine = (" @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" + + " inputSchemaResource = \"%s\",\n" + + " outputSchemaResource = \"%s\")").formatted(register, group.toLowerCase(Locale.ROOT), interfaceId, inputSchemaClasspath, outputSchemaClasspath); } else { - toolHintLine = " @ToolHint(register = %s)".formatted(register); + toolHintLine = " @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId); } String serviceInterfaceContent = """ package %s.usecase; - import org.springframework.ai.mcp.annotation.McpTool; + import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; import %s.dto.%sRequest; import %s.dto.%sResponse; @@ -279,7 +298,6 @@ public class ToolScaffolder { import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; - import java.util.Map; import %s.converter.%sConverter; import %s.%s.io.%s_I; %s @@ -307,7 +325,7 @@ public class ToolScaffolder { private final %sConverter converter; @Override - public Object execute(%sRequest req) { + public %sResponse execute(%sRequest req) { log.info("[MCI Tool] {} 요청 수신.", "%s"); try { // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) @@ -319,10 +337,18 @@ public class ToolScaffolder { mciReq, Object.class ); - return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS"); + %sResponse response = new %sResponse(); + response.setResultCode("SUCCESS"); + response.setResultMessage(resTransfer.getBody() != null + ? "MCI call completed." + : "MCI call completed without a response body."); + return response; } catch (Exception e) { log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); - return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + %sResponse response = new %sResponse(); + response.setResultCode("ERROR"); + response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error"); + return response; } } } @@ -344,12 +370,27 @@ public class ToolScaffolder { (clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"), baseName, baseName, + baseName, toolName, interfaceId, - interfaceId + interfaceId, + baseName, + baseName, + baseName, + baseName ); + String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", "."); + serviceImplContent = serviceImplContent + .replace("import " + mciIoPackage + ".io." + interfaceId + "_I;", + "import " + mciIoPackage + ".io." + interfaceId + "_I;\nimport " + mciIoPackage + ".io." + interfaceId + "_O;") + .replace("Transfer resTransfer", "Transfer<" + interfaceId + "_O> resTransfer") + .replace("Object.class", interfaceId + "_O.class") + .replace("response.setResultCode(\"SUCCESS\");", + "if (resTransfer.getBody() != null) {\n response = converter.toResponse(resTransfer.getBody());\n }\n response.setResultCode(\"SUCCESS\");"); } else { - serviceImplContent = """ + serviceImplContent = "HTTP".equalsIgnoreCase(routingType) + ? httpUseCaseImplContent(bizPackage, baseName, interfaceId, author, createDate) + : """ package %s.usecase.impl; import %s.dto.%sRequest; @@ -383,9 +424,16 @@ public class ToolScaffolder { private final %sConverter converter; @Override - public Object execute(%sRequest req) { + public %sResponse execute(%sRequest req) { // %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); - return executeLegacy("%s", "%s", req); // Or pass legacyRequest + Object legacyResponse = executeLegacy("%s", "%s", req); // Or pass legacyRequest + if (legacyResponse instanceof %sResponse response) { + return response; + } + %sResponse response = new %sResponse(); + response.setResultCode("SUCCESS"); + response.setResultMessage("Legacy call completed."); + return response; } } """.formatted( @@ -402,6 +450,10 @@ public class ToolScaffolder { baseName, baseName, baseName, + baseName, + baseName, + baseName, + baseName, routingType, interfaceId ); } @@ -441,6 +493,7 @@ public class ToolScaffolder { private String content; } """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate); Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent); String mciResContent = """ @@ -467,6 +520,7 @@ public class ToolScaffolder { // TODO: Add response fields here } """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate); Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent); String converterContent = """ @@ -518,6 +572,7 @@ public class ToolScaffolder { baseName, interfaceId, baseName, interfaceId ); + converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId); Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); log.append("\n=========================================\n"); @@ -559,7 +614,7 @@ public class ToolScaffolder { public class Mci%sClient { private final AxhubMciComponent mci; - public Transfer 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); } } @@ -604,6 +659,7 @@ public class ToolScaffolder { private String content; } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + legacyReqContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyRequest", inputFields, author, createDate, true); Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent); String legacyResContent = """ @@ -630,6 +686,7 @@ public class ToolScaffolder { // TODO: Add legacy response fields here } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + legacyResContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyResponse", outputFields, author, createDate, true); Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent); String converterContent = """ @@ -679,6 +736,7 @@ public class ToolScaffolder { bizPackage, baseName, author, createDate, createDate, author, baseName, baseName, baseName, baseName, baseName, baseName, baseName ); + converterContent = legacyConverterContent(bizPackage, baseName); Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); log.append("\n=========================================\n"); @@ -732,6 +790,17 @@ public class ToolScaffolder { log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n"); } + Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json")); + Files.createDirectories(mockResponsePath.getParent()); + Files.writeString(mockResponsePath, mockResponseContent(outputFields)); + + Path generatedTestDir = rootDir.resolve(Paths.get(moduleName, "src/test/java/io/shinhanlife/dap/mcc/biz", group.toLowerCase(), "usecase")); + Files.createDirectories(generatedTestDir); + Path generatedTestPath = generatedTestDir.resolve(baseName + "UseCaseTest.java"); + Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName)); + log.append("[Mock Response] ").append(mockResponsePath).append("\\n"); + log.append("[Unit Test] ").append(generatedTestPath).append("\\n"); + log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n"); log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); return log.toString(); @@ -744,6 +813,223 @@ public class ToolScaffolder { .toLowerCase(Locale.ROOT); } + private static String dtoContent(String packageName, String className, List fields, + String author, String createDate, boolean request) { + String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage")); + if (!request) { + body = " private String resultCode;\n\n private String resultMessage;\n" + body; + } + return """ + package %s; + + import com.fasterxml.jackson.annotation.JsonInclude; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %s { + %s} + """.formatted(packageName, className, body); + } + + private static String mciIoContent(String packageSuffix, String className, List fields, + String author, String createDate) { + return """ + package %s.%s.io; + + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; + + @Data + public class %s { + %s} + """.formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields)); + } + + private static String httpUseCaseImplContent(String bizPackage, String baseName, String interfaceId, String author, String createDate) { + return """ + package %s.usecase.impl; + + import %s.converter.%sConverter; + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.legacy.%sLegacyRequest; + import %s.legacy.%sLegacyResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent; + import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpDomain; + import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import org.springframework.stereotype.Service; + + /** + * HTTP Tool implementation. Calls the Glow HTTP adapter through AxhubHttpComponent. + * Configure the target domain in AxhubHttpDomain and glow.communication.http.api-list before use. + */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase { + + private final %sConverter converter; + private final AxhubHttpComponent http; + + @Override + public %sResponse execute(%sRequest req) { + %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); + %sLegacyResponse legacyResponse = http.call( + AxhubHttpDomain.SAMPLE, + "/%s", + legacyRequest, + %sLegacyResponse.class + ); + + %sResponse response = converter.toResponse(legacyResponse); + response.setResultCode("SUCCESS"); + response.setResultMessage("HTTP API call completed."); + return response; + } + } + """.formatted( + bizPackage, bizPackage, baseName, bizPackage, baseName, bizPackage, baseName, + bizPackage, baseName, bizPackage, baseName, bizPackage, baseName, + baseName, baseName, baseName, baseName, baseName, baseName, baseName, + interfaceId, baseName, baseName); + } + private static String mciConverterContent(String bizPackage, String baseName, String mciPackage, String interfaceId) { + return """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.%s.io.%s_I; + import %s.%s.io.%s_O; + import org.mapstruct.Mapper; + + @Mapper(componentModel = "spring") + public interface %sConverter { + %s_I toLegacyRequest(%sRequest request); + %sRequest toRequest(%s_I mciRequest); + %sResponse toResponse(%s_O mciRes); + } + """.formatted(bizPackage, bizPackage, baseName, bizPackage, baseName, + BASE_PACKAGE, mciPackage, interfaceId, BASE_PACKAGE, mciPackage, interfaceId, + baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId); + } + + private static String legacyConverterContent(String bizPackage, String baseName) { + return """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.legacy.%sLegacyRequest; + import %s.legacy.%sLegacyResponse; + import org.mapstruct.Mapper; + import org.mapstruct.ReportingPolicy; + + @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) + public interface %sConverter { + %sLegacyRequest toLegacyRequest(%sRequest request); + %sRequest toRequest(%sLegacyRequest legacyRequest); + %sResponse toResponse(%sLegacyResponse legacyResponse); + } + """.formatted( + bizPackage, bizPackage, baseName, bizPackage, baseName, + bizPackage, baseName, bizPackage, baseName, + baseName, baseName, baseName, baseName, baseName, baseName, baseName); + } + private static String fieldLines(List 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("\"", "\\\""); + source.append(" @Schema(description = \"").append(description).append("\", example = \"") + .append(example).append("\""); + if (field.required()) { + source.append(", requiredMode = Schema.RequiredMode.REQUIRED"); + } + source.append(")\n private ").append(type).append(' ').append(fieldName).append(";\n\n"); + } + return source.toString(); + } + private static String supportedType(String type) { + return switch (type == null ? "String" : type) { + case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; + default -> throw new IllegalArgumentException("Unsupported field type: " + type); + }; + } + + + private static String mockResponseContent(List 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-") @@ -756,7 +1042,7 @@ public class ToolScaffolder { String[] words = normalizedName.split("\\s+"); String service = words[0]; String action = words.length == 1 ? "execute" : words[words.length - 1]; - return "%s.%s.%s.%s".formatted( + return "%s_%s_%s_%s".formatted( pod.toLowerCase(Locale.ROOT), group.toLowerCase(Locale.ROOT), service, @@ -784,4 +1070,4 @@ public class ToolScaffolder { } return result.toString(); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java index ebc4ed13..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.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()); } @@ -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.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()); } @@ -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.springframework.ai.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); @@ -57,8 +56,8 @@ public class ToolSchemaResolver { /** * Retained for callers that use only explicit output schemas. */ - public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) { - return resolveOutput(function, null); + public Map resolveOutput(org.springaicommunity.mcp.annotation.McpTool function) { + 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/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/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 c3f42a2b..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.springframework.ai.mcp.annotation.McpTool; -import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; -import io.shinhanlife.dap.lib.config.McpProperties; -import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; +import io.shinhanlife.dap.lib.mcp.ToolExecutionResult; +import io.shinhanlife.dap.lib.mcp.McpToolExecutionService; import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; -import io.shinhanlife.dap.lib.util.ToolSchemaResolver; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; import java.util.List; import java.util.Map; -import java.util.Set; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.aop.support.AopUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.ResponseEntity; -import org.springframework.util.ClassUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -45,26 +17,20 @@ import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Slf4j +/** Legacy REST adapter for Tool Pod execution. */ @RestController @RequestMapping("/") @RequiredArgsConstructor public class BusinessToolController { - private final ApplicationContext applicationContext; - private final ObjectMapper objectMapper; - private final McpProperties mcpProperties; private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; - private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; + private final McpToolExecutionService toolExecutionService; - private final ToolSchemaResolver toolSchemaResolver; - // 내부 조회용 로컬 Tool 목록 엔드포인트 @GetMapping("/mcp/api/v1/tools/local") public List 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/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/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..e5925405 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,38 @@ logging: pattern: console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n" -# 2. Redis 공통 설정 (Gateway 연동 및 캐싱) spring: data: redis: - host: 127.0.0.1 # TODO: 실제 Redis 망 IP로 변경 - port: 6379 # TODO: 실제 Redis 포트로 변경 - password: "" # TODO: 실제 비밀번호 입력 + host: ${SPRING_DATA_REDIS_HOST:127.0.0.1} + port: ${SPRING_DATA_REDIS_PORT:6379} + password: ${SPRING_DATA_REDIS_PASSWORD:} -# 3. 신한라이프 Glow Framework 통신 (MCI / EAI) 설정 glow: - communication: #Communication 설정 - common: - env-type: D # 대내표준 헤더의 환경 타입정보 (D: 개발, T: 테스트, P: 운영) - http: # HTTP 비표준 통신 설정 - connection-timeout: 5 # 연결 타임아웃 시간 (초 단위) - read-timeout: 5 # 읽기 타임아웃 시간 (초 단위) - mci: # 개발계 대내 MCI 통신 설정 - host: https://dev-ichmci.shinhanlife.co.kr - port: 26160 + communication: + http: + connection-timeout: 5 + read-timeout: 5 + # HTTP Tool target catalog. Replace or add entries after the business endpoint is agreed. + api-list: + - name: sample + domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8099} + path: "" + method: GET + biz-pod: false + mci: uri: /ntl_mci/dap_rcv receive-uri: /itrf/mciReceive connection-timeout: 300 read-timeout: 300 encoding: UTF-8 - extmci: # 대외MCI 통신 설정 - host: http://host.docker.internal # 대외MCI 호스트 주소 - port: 8080 # 대외MCI 포트 번호 - uri: /extmci # 대외MCI URI 정보 - json-uri: /extmciJson # 대외MCI JSON방식 URI 정보 - receive-uri: /app/extMciReceive # 대외MCI 수신 URI - connection-timeout: 5 # 연결 타임아웃 시간 (초 단위) - read-timeout: 30 # 읽기 타임아웃 시간 (초 단위) - encoding: EUC-KR # 대외MCI 인코딩 정보 - eai: # EAI 통신 설정 - host: tcp://host.docker.internal # EAI 호스트 주소 (TODO: 실제 주소로 변경) - port: 9999 # EAI 포트 번호 + extmci: + uri: /extmci + json-uri: /extmciJson + receive-uri: /app/extMciReceive + connection-timeout: 5 + read-timeout: 30 + encoding: EUC-KR websocket: - endpoint: "/ws-glow" - allowed-origins: "*" + endpoint: /ws-glow + allowed-origins: "*" \ No newline at end of file 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/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/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-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..37b321be --- /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 new file mode 100644 index 00000000..26ddc40a --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java @@ -0,0 +1,62 @@ +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(); + McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties()); + registry.initialize(); + return new McpToolExecutionService(registry, objectMapper, + 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/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-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/PodScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java new file mode 100644 index 00000000..5d708d4b --- /dev/null +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/PodScaffolderTest.java @@ -0,0 +1,53 @@ +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")); + } + + @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}")); + 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-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..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 @@ -1,15 +1,21 @@ 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 java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class ToolScaffolderTest { + @TempDir + 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, @@ -19,11 +25,10 @@ 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("version = \"1.0.0\"")); - assertTrue(useCase.contains("timeoutMillis = 300000L")); - assertTrue(response.contains("")); - assertTrue(response.contains("")); + 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;")); } @Test void usesWasModuleNameAsToolPodPrefix() throws Exception { @@ -36,6 +41,127 @@ 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 + 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"))); 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 generatesRequestDtoFromDeclaredFields() 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(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"); + String implementation = Files.readString(implementationPath); + 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/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;")); + 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);")); + } + @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")); + } + + @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-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..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 @@ -4,11 +4,13 @@ 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; import org.junit.jupiter.api.Test; -import org.springframework.ai.mcp.annotation.McpTool; +import org.springaicommunity.mcp.annotation.McpTool; class ToolSchemaResolverTest { @@ -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"); @@ -60,31 +57,27 @@ 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; } } + @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 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..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 @@ -18,19 +18,19 @@ 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") + @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 93e37aee..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 { @@ -90,7 +108,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-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..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 @@ -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; @@ -55,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-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 +} diff --git a/dap-was-oth/build.gradle b/dap-was-oth/build.gradle index f2effdbd..85b96419 100644 --- a/dap-was-oth/build.gradle +++ b/dap-was-oth/build.gradle @@ -1,13 +1,10 @@ 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') implementation 'org.apache.poi:poi-ooxml:5.3.0' } - -dependencies { - compileOnly 'org.projectlombok:lombok:1.18.32' - annotationProcessor 'org.projectlombok:lombok:1.18.32' -} 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..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 @@ -1,12 +1,12 @@ 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.*; 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 9d0fec93..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 @@ -1,13 +1,13 @@ 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.*; 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 7c92a949..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 @@ -1,14 +1,14 @@ 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.*; 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 c55be3a0..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 @@ -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; @@ -8,9 +8,9 @@ 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: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/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..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 @@ -1,18 +1,18 @@ 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.*; 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 f3eee3e5..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 @@ -1,14 +1,14 @@ 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.*; 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 1847f93c..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 @@ -1,14 +1,14 @@ 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.*; 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 8ff9ce8a..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 @@ -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; @@ -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 7eec1a6e..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 @@ -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; @@ -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 63015f61..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 @@ -1,14 +1,14 @@ 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.*; 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 83d45d03..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 @@ -1,11 +1,11 @@ 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.*; public interface Onnba3011UseCase { - @McpTool(name = "oth.oth.onnba3011.call", description = "Onnba3011 호출 툴") - @ToolHint(categoryKey = "oth") + @McpTool(name = "oth_oth_onnba3011_call", description = "Onnba3011 호출 툴") + @ToolHint(categoryKey = "oth", register = false) Object execute(Onnba3011Request req); } 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/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/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..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 @@ -1,13 +1,13 @@ 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; 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 ba628bd5..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 @@ -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; @@ -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 new file mode 100644 index 00000000..1eb10081 --- /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_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/TeamMemberUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java index 5bc204bd..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 @@ -1,12 +1,12 @@ 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; 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 1252d6a4..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 @@ -1,12 +1,12 @@ 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.*; 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/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/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..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 @@ -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; @@ -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 00a7c491..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 @@ -1,12 +1,12 @@ 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; 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-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-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 new file mode 100644 index 00000000..b5f08f21 --- /dev/null +++ b/dap-was-oth/src/main/resources/application-prod.yml @@ -0,0 +1,16 @@ +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} + 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..ab1c6908 --- /dev/null +++ b/dap-was-oth/src/main/resources/application-test.yml @@ -0,0 +1,16 @@ +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} + tool: + url: ${AXHUB_TOOL_URL} 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/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' -} 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/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..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 @@ -1,11 +1,11 @@ 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.*; 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/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 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()); + } +} 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 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