Initial commit

This commit is contained in:
jade
2026-08-14 18:16:14 +09:00
commit 22f4fab58d
656 changed files with 22614 additions and 0 deletions

268
README.md Normal file
View File

@@ -0,0 +1,268 @@
# 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` |