# DAP WAS Tool Pods 신한라이프 업무 시스템과 MCP(Model Context Protocol) 클라이언트를 연결하는 독립형 Tool WAS 프로젝트입니다. 이 저장소에는 Gateway가 포함되어 있지 않습니다. 각 Tool Pod가 직접 MCP Streamable HTTP와 REST 실행 API를 제공하고, 업무 요청은 `UseCase → Converter → MCI/EAI 연동`으로 처리합니다. > 이 문서는 현재 `main`의 구현과 설정을 기준으로 합니다. 과거 `dap-gateway`, Chat API, SSE, 외부 Tool Registry/Heartbeat 관련 문서는 현재 저장소의 동작 범위가 아니므로 포함하지 않습니다. ## 구성 ```text MCP Client ├─ Streamable HTTP: /mcp └─ REST: POST /mcp/{tool-name} │ ▼ Tool Pod (dap-was-oth 또는 dap-was-sms) │ ├─ LocalToolScanner: @McpTool / @McpFunction 메타데이터 생성 ├─ BusinessToolController: 입력 검증·DTO 변환·동적 실행 ├─ ToolManifestController: /tool-manifest 제공 └─ UseCase → Converter → MCI/EAI Client → 대상 시스템 ``` ## Gradle 모듈 | 모듈 | 역할 | 기본 포트 | |---|---|---:| | `dap-was-lib` | MCP 어노테이션, 스캐너, 실행 Controller, Manifest, Schema, MCI/EAI/로깅/보안 공통 기능 | - | | `dap-was-oth` | 공통·기타·샘플·SOL 업무 Tool Pod | 8084 | | `dap-was-sms` | SMS/알림 업무 Pod | 8082 | 기술 기준은 Java 21, Spring Boot 4.0.5, Gradle Wrapper 8.14.3, Spring AI MCP Server WebMVC, Redis, MapStruct, MyBatis, Resilience4j입니다. ## 현재 제공 API 아래 API는 각 Tool Pod가 직접 제공합니다. OTH Pod의 로컬 주소는 `http://localhost:8084`, SMS Pod는 `http://localhost:8082`입니다. | 목적 | 메서드 | 경로 | 구현 | |---|---|---|---| | MCP Streamable HTTP 전송 | MCP 프로토콜 | `/mcp` | `ToolMcpServerConfiguration` | | Pod에서 스캔한 Tool 메타데이터 조회 | `GET` | `/mcp/api/v1/tools/local` | `BusinessToolController` | | 이름으로 Tool 직접 실행 | `POST` | `/mcp/{name}` | `BusinessToolController` | | Pod 소유 Manifest 조회 | `GET` | `/tool-manifest` | `ToolManifestController` | `GET /tool-manifest`는 `If-None-Match` 요청 헤더를 지원하며, 내용이 바뀌지 않으면 `304 Not Modified`를 반환합니다. 응답에는 bundle ID, revision, Tool 목록, 입력 Schema, 실행 endpoint, annotation/meta 정보가 포함됩니다. Manifest는 `LocalToolScanner`의 전체 스캔 목록을 사용하므로 `visible = false` 또는 `register = false`인 항목도 포함될 수 있습니다. `GET /mcp/api/v1/tools/local`은 Manifest 검증이나 MCP 세션을 열지 않고, 현재 Pod에서 스캔한 `ToolMetadata` 목록을 반환합니다. ### REST 실행 예시 Tool 이름은 `@McpFunction.name` 값입니다. 예를 들어 OTH Pod의 `oth.smp.weather.inquiry`는 다음처럼 호출합니다. ```powershell $headers = @{ 'trace-id' = 'trace-local-001' 'request-id' = 'request-local-001' } Invoke-RestMethod ` -Method Post ` -Uri 'http://localhost:8084/mcp/oth.smp.weather.inquiry' ` -Headers $headers ` -ContentType 'application/json' ` -Body '{"city":"Seoul"}' ``` Controller는 이름을 찾은 뒤 요청 JSON을 첫 번째 DTO 매개변수로 변환합니다. 입력 Schema 검증 실패는 `422 INVALID_PARAM`, 존재하지 않는 Tool은 `404 TOOL_NOT_FOUND`, 실행 예외는 `502 TOOL_ERROR` 응답입니다. `trace-id`와 `request-id`는 성공 응답 헤더로 다시 전달됩니다. ## Tool 검색과 MCP 노출 규칙 애플리케이션 기동 시 `LocalToolScanner`는 Spring Bean에서 `@McpTool`과 `@McpFunction` 메타데이터를 읽어 로컬 Tool 목록을 만듭니다. 기동 완료 후 `ToolPodMcpToolSynchronizer`는 이 목록 중 `visible = true`인 Tool만 MCP SDK 서버에 추가합니다. | 속성 | 현재 구현에서의 의미 | |---|---| | `visible` | `false`이면 MCP SDK의 Tool 등록에서 제외됩니다. | | `register` | 스캐너 메타데이터의 `isRegistered` 값과 내부 `registeredTools` 목록에만 반영됩니다. 현재 저장소에는 외부 Registry 전송 구현이 없습니다. | | `namespace` | 비어 있지 않으면 Tool 이름 앞에 `{namespace}_`가 붙습니다. 기본 설정은 빈 문자열입니다. | | `enabled` | Manifest의 `_meta.enabled` 값으로 노출됩니다. 현재 synchronizer는 이 값으로 별도 필터링하지 않습니다. | | `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint` | MCP Tool annotation과 Manifest annotation에 반영됩니다. | `POST /mcp/{name}`의 동적 실행은 `visible` 및 `register` 값으로 차단하지 않습니다. 따라서 직접 호출을 막아야 하는 Tool은 네트워크 경계와 별도 인증·인가 정책으로 보호해야 합니다. ### 현재 Tool 선언 현황 `dap-was-oth`에는 실제 `@McpFunction` 선언이 17개 있습니다. 도메인은 다음과 같습니다. | 도메인 | 예시 Tool 이름 | 내용 | |---|---|---| | `cmm` | `oth.cmm.claim.search`, `oth.cmm.customer.detail`, `oth.cmm.meta.table` | 공통·고객·계약·청구·메타 기능 | | `smp` | `oth.smp.weather.inquiry`, `oth.smp.exchange-rate.inquiry` | 샘플·조회 기능 | | `sol` | `oth.sol.request.list`, `oth.sol.request.detail` | SOL 요청 조회 | `dap-was-oth`에는 `categoryKey = "oth"`인 `Onnba3011UseCase`도 있으나, 현재 `@McpFunction` 선언은 없습니다. `dap-was-sms`도 `@McpTool(routingType = "EAI", categoryKey = "notification")`은 선언되어 있지만, `SmsToolUseCase`/구현체에 `@McpFunction`이 없습니다. 따라서 현 상태에서 두 영역은 스캐너, `/tool-manifest`, MCP SDK에 노출되는 호출 가능 Tool을 만들지 않습니다. MCP Tool로 제공하려면 각 계약 메서드에 `@McpFunction`을 선언해야 합니다. ## Tool 개발 방식 Tool 그룹은 인터페이스에 `@McpTool`, Agent가 호출하는 메서드는 `@McpFunction`을 선언합니다. `@McpTool`은 Spring `@Component` 별칭이므로 Tool 인터페이스와 구현체는 Spring Bean으로 구성되어야 합니다. ```java @McpTool(routingType = "MCI", categoryKey = "claim") public interface ClaimInquiryUseCase { @McpFunction( name = "oth.claim.inquiry.detail", displayName = "청구 상세 조회", description = "청구 번호로 청구 상세를 조회합니다.", mappingId = "CLM00000001", readOnlyHint = true ) ClaimInquiryResponse inquire(ClaimInquiryRequest request); } ``` 구현체에는 업무 흐름만 두고, Tool DTO와 레거시 인터페이스 DTO의 변환은 Converter에 둡니다. ```java @Service @RequiredArgsConstructor class ClaimInquiryUseCaseImpl implements ClaimInquiryUseCase { private final ClaimInquiryConverter converter; private final MciClaimClient client; @Override public ClaimInquiryResponse inquire(ClaimInquiryRequest request) { ClaimMciRequest legacyRequest = converter.toMciRequest(request); ClaimMciResponse legacyResponse = client.call(legacyRequest); return converter.toResponse(legacyResponse); } } ``` `@McpFunction.name`은 소문자 점 표기 형식으로 작성합니다. 현재 선언은 `oth.cmm.claim.search`, `oth.smp.weather.inquiry`처럼 `{pod}.{domain}.{service}.{action}` 패턴을 사용합니다. `validateMcpToolNames` Gradle 작업은 모든 Tool 모듈을 대상으로 이름 형식과 중복을 검사하며, 패키징 빌드 전에 실행됩니다. ## Input/Output Schema 입력 Schema는 다음 우선순위로 결정됩니다. 1. `inputSchemaResource`에 지정한 classpath JSON Schema 2. `inputSchema`에 인라인으로 지정한 JSON Schema 3. 요청 DTO의 `@McpValidation`을 이용한 자동 생성 Schema 출력 검증은 선택 사항입니다. 다음 중 하나가 있을 때만 반환값을 검증합니다. 1. `outputSchemaResource` 2. `outputSchema` 3. 반환 DTO의 `@McpOutputSchema`와 `@McpValidation` 복잡한 Schema 리소스는 Tool 모듈에 둡니다. 현재 OTH의 청구 검색 예제는 다음 리소스를 사용합니다. ```text dap-was-oth/src/main/resources/tool-schemas/cmm/ ├─ claim-search-resource-input-schema.json └─ claim-search-resource-output-schema.json ``` 입력 검증에는 JSON Schema Draft 7이 사용됩니다. 출력 Schema 검증에 실패하면 `500 INVALID_TOOL_RESPONSE`을 반환합니다. ## 로컬 실행 ### 사전 조건 - JDK 21 - Docker (Redis 또는 MCI mock을 사용할 경우) - Gradle Wrapper 사용 권장 로컬 프로필은 기본값이며, 두 Pod 모두 H2 메모리 DB와 P6Spy를 설정합니다. Pod URL은 `AXHUB_TOOL_URL` 환경 변수로 설정하며, 지정하지 않으면 해당 `server.port`의 localhost 주소를 사용합니다. ```powershell # 필수: Redis 기동 (캐시 및 세션 처리용) $env:ACTIVE_PROFILE = 'local' docker compose up -d redis # OTH Tool Pod 실행 $env:SPRING_PROFILES_ACTIVE = 'local' $env:AXHUB_TOOL_URL = 'http://localhost:8084' .\gradlew.bat :dap-was-oth:bootRun # SMS Tool Pod 실행 (별도 PowerShell) $env:SPRING_PROFILES_ACTIVE = 'local' $env:AXHUB_TOOL_URL = 'http://localhost:8082' .\gradlew.bat :dap-was-sms:bootRun ``` 실행 후 OTH Pod에서 다음 URL로 현재 스캔된 메타데이터와 Manifest를 확인할 수 있습니다. ```text http://localhost:8084/mcp/api/v1/tools/local http://localhost:8084/tool-manifest http://localhost:8084/tool-test-console.html ``` `tool-test-console.html`은 공통 라이브러리의 정적 리소스입니다. `/tool-manifest`에서 Tool과 입력 Schema를 읽어 요청 JSON을 만들고, 현재 Pod의 `/mcp/{toolName}`으로 호출합니다. 저장한 테스트 케이스는 브라우저 `localStorage`에 보관됩니다. ## 테스트와 빌드 ```powershell # 전체 테스트 .\gradlew.bat test # 공통 라이브러리 테스트 .\gradlew.bat :dap-was-lib:test # OTH Tool 테스트 .\gradlew.bat :dap-was-oth:test # Tool 이름 규칙 및 중복 검증 .\gradlew.bat validateMcpToolNames # 패키징 전 전체 빌드 .\gradlew.bat clean build ``` 테스트는 공통 MCP Schema/Manifest/Header 처리, Glow MCI 파서, Tool 이름 검증과 OTH의 청구·SOL·MCI 변환을 다룹니다. SMS 모듈에는 현재 별도 테스트 소스가 없습니다. ## Docker Compose 현재 Compose 서비스와 호스트 포트는 다음과 같습니다. | 서비스 | 컨테이너 포트 | 호스트 포트 | |---|---:|---:| | `redis` | 6379 | 6379 | | `was-sms` | 8082 | 8282 | | `was-oth` | 8084 | 8284 | Compose의 Pod URL은 컨테이너 DNS 이름을 사용합니다. ```text was-sms: http://was-sms:8082 was-oth: http://was-oth:8084 ``` ### Docker 컨테이너 기동 별도의 CI/CD 러너나 외부 의존성(MCI Mock 등) 없이 독립적으로 실행 가능하도록 구성되어 있습니다. `docker-compose.yml`을 통해 Redis 및 각 Pod 컨테이너를 구동할 수 있습니다. ## 설정 | 설정 | 위치/환경 변수 | 설명 | |---|---|---| | Pod 포트 | `server.port` 또는 `PORT` | SMS 8082, OTH 8084 | | Pod 외부 URL | `AXHUB_TOOL_URL` | 스캐너가 Tool endpoint를 만들 때 사용 | | MCP namespace | `mcp.namespace` | Tool 이름 앞에 `{namespace}_`를 붙임 | | Manifest bundle | `mcp.manifest.bundle-id` | SMS는 `tool-sms`, OTH는 `tool-oth` | | Manifest 이름 접두사 | `mcp.manifest.name-prefix` | 지정 시 모든 Manifest Tool 이름이 이 접두사로 시작해야 함 | | 활성 프로필 | `SPRING_PROFILES_ACTIVE` | 기본값 `local`, 선택값 `dev` | `mcp.security.tenant-domains` 설정은 각 Pod의 YAML에 존재하지만, 현재 `McpProperties`와 `BusinessToolController`에는 이를 이용해 호출을 차단하는 로직이 없습니다. 문서상 권한 기능으로 간주하지 말고, 운영 노출 시 별도 인증·인가 계층을 적용해야 합니다. ## 보안과 운영 주의사항 - `BusinessToolController`는 요청 파라미터와 결과를 로그로 남깁니다. Tool 입력·응답에는 주민번호, 계좌번호, 전화번호, 인증값 등 민감정보를 포함하지 않도록 설계하고 공통 마스킹 적용 여부를 검토해야 합니다. - `employee-id` 헤더는 Controller가 수신하지만 현재 실행 로직에서 사용하지 않습니다. 이 헤더만으로 인증·인가가 수행된다고 가정하면 안 됩니다. - `mcp.security.tenant-domains`, `requiresApproval`, `register`는 현재 독립 WAS에서 실행 차단 정책을 구현하지 않습니다. - 외부 MCI/EAI 대상은 local/dev 설정과 실제 네트워크 정책을 별도로 점검해야 합니다. ## 참고 소스 | 주제 | 위치 | |---|---| | REST 실행 및 로컬 목록 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/BusinessToolController.java` | | Manifest API | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/ToolManifestController.java` | | MCP Streamable HTTP | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java` | | MCP Tool 동기화 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java` | | Tool 스캔 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/usecase/LocalToolScanner.java` | | Tool 어노테이션 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java`, `McpFunction.java` | | OTH 업무 Tool | `dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/` | | SMS Tool | `dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/` | | 컨테이너 구성 | `docker-compose.yml`, `dap-was-oth/Dockerfile`, `dap-was-sms/Dockerfile` |