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