From 00f800aff99c6462796587a28aa9d69e08eb740c Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 18 Aug 2026 09:09:09 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20README=20=ED=98=84=ED=96=89=ED=99=94=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B3=A0=EC=95=84=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 397 ++++++++++-------- .../DtoExcelDownloadControllerTest.java | 32 -- 2 files changed, 217 insertions(+), 212 deletions(-) delete mode 100644 dap-was-cus/src/test/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadControllerTest.java diff --git a/README.md b/README.md index 5df6d7a7c..ca7778d7e 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,81 @@ -# DAP WAS Tool Pods +# DAP WAS MCP Tool Pods -신한라이프 업무 시스템과 MCP(Model Context Protocol) 클라이언트를 연결하는 독립형 Tool WAS 프로젝트입니다. 이 저장소에는 Gateway가 포함되어 있지 않습니다. 각 Tool Pod가 직접 MCP Streamable HTTP와 REST 실행 API를 제공하고, 업무 요청은 `UseCase → Converter → MCI/EAI 연동`으로 처리합니다. +신한라이프 업무 기능을 MCP(Model Context Protocol) Tool로 제공하는 Java 멀티 모듈 프로젝트입니다. 각 업무 모듈은 독립 실행 가능한 Spring Boot 애플리케이션이며, MCP Streamable HTTP와 REST 실행 API를 함께 제공합니다. -> 이 문서는 현재 `main`의 구현과 설정을 기준으로 합니다. 과거 `dap-gateway`, Chat API, SSE, 외부 Tool Registry/Heartbeat 관련 문서는 현재 저장소의 동작 범위가 아니므로 포함하지 않습니다. +이 저장소에는 Gateway 애플리케이션이 포함되어 있지 않습니다. Tool Pod는 설정된 외부 Gateway에 Tool 등록을 시도하지만, Tool 조회와 직접 실행은 각 Pod가 자체적으로 처리합니다. -## 구성 +## 기술 기준 + +- Java 21 +- Gradle Wrapper 8.14.3 +- Spring Boot 3.5.11 +- MCP Java SDK 2.0.0 +- Spring AI Community MCP Annotations 0.9.0 +- Jackson 2.20.1 +- MapStruct, MyBatis, Redis, Kafka, Resilience4j +- JUnit 5 + +## 모듈 구성 + +| 모듈 | 역할 | 기본 포트 | Tool 수 | +|---|---|---:|---:| +| `dap-was-lib` | MCP 서버, Tool 스캔·실행, Schema, Manifest, 보안, MCI/EAI/HTTP 연동 공통 기능 | - | - | +| `dap-was-cus` | 고객·CRM·VOC·웹 콘텐츠 관리 Tool | 8084 | 51 | +| `dap-was-sal` | 영업·청구·인수·동의·현장지원 Tool | 8082 | 52 | +| `dap-was-pro` | 상품·계약·고객·GA 설계사 Tool | 8085 | 50 | +| `dap-was-sys` | IAM·시스템 상태·공지·점검·배포 Tool | 8086 | 50 | + +총 203개의 `@McpTool` 선언과 203개의 V17 Tool YAML 정의가 있습니다. + +현재 업무 구현은 개발·연동 검증 단계입니다. `dap-was-sal`의 `cmm_claim_search`와 `cmm_memo_retriever`는 각각 MCI와 HTTP Client 흐름을 사용하며, 나머지 Tool은 외부 시스템을 변경하지 않는 모의 응답을 중심으로 구현되어 있습니다. + +## 처리 구조 ```text -MCP Client - ├─ Streamable HTTP: /mcp - └─ REST: POST /mcp/{tool-name} +MCP Client 또는 REST Client + ├─ MCP Streamable HTTP: /mcp + └─ REST: POST /mcp/{toolName} │ ▼ -Tool Pod (dap-was-oth 또는 dap-was-sms) - │ - ├─ LocalToolScanner: @McpTool / @McpFunction 메타데이터 생성 - ├─ BusinessToolController: 입력 검증·DTO 변환·동적 실행 - ├─ ToolManifestController: /tool-manifest 제공 - └─ UseCase → Converter → MCI/EAI Client → 대상 시스템 +Tool Pod + ├─ McpToolMethodRegistry + │ └─ Spring Bean의 @McpTool 메서드 탐색 및 실행 메서드 캐시 + ├─ ToolRegistryHeartbeatSender + │ ├─ Tool 메타데이터 생성 + │ ├─ tool-definitions YAML 병합 + │ └─ 외부 Gateway 등록 시도 + ├─ McpToolExecutionService + │ ├─ 입력 Schema 검증 + │ ├─ 요청 DTO 변환 및 Tool 호출 + │ └─ 출력 Schema 검증 + └─ UseCase → Converter → MCI/EAI/HTTP Client 또는 Mock 응답 ``` -## Gradle 모듈 +애플리케이션 시작 시 다음 순서로 Tool이 준비됩니다. -| 모듈 | 역할 | 기본 포트 | -|---|---|---:| -| `dap-was-lib` | MCP 어노테이션, 스캐너, 실행 Controller, Manifest, Schema, MCI/EAI/로깅/보안 공통 기능 | - | -| `dap-was-oth` | 공통·기타·샘플·SOL 업무 Tool Pod | 8084 | -| `dap-was-sms` | SMS/알림 업무 Pod | 8082 | +1. `ToolDefinitionRepository`가 `classpath*:tool-definitions/**/*.yml`을 읽고 V17 필수 항목을 검증합니다. +2. `ToolRegistryHeartbeatSender`가 `@McpTool` 메서드를 스캔하고 YAML 정의를 병합해 `ToolMetadata`를 생성합니다. +3. `McpToolMethodRegistry`가 실제 호출 가능한 Bean과 메서드를 Tool 이름으로 캐시합니다. +4. `ToolPodMcpToolSynchronizer`가 Tool을 MCP SDK 서버에 등록합니다. +5. REST와 MCP 요청은 공통 `McpToolExecutionService`를 통해 실행됩니다. -기술 기준은 Java 21, Spring Boot 4.0.5, Gradle Wrapper 8.14.3, Spring AI MCP Server WebMVC, Redis, MapStruct, MyBatis, Resilience4j입니다. +## 제공 API -## 현재 제공 API +각 업무 Pod가 동일한 API 구조를 제공합니다. -아래 API는 각 Tool Pod가 직접 제공합니다. OTH Pod의 로컬 주소는 `http://localhost:8084`, SMS Pod는 `http://localhost:8082`입니다. +| 목적 | 메서드 | 경로 | +|---|---|---| +| MCP Streamable HTTP | MCP 프로토콜 | `/mcp` | +| MCP 메시지 전송 | MCP 프로토콜 | `/mcp/message` | +| 로컬 Tool 메타데이터 조회 | `GET` | `/mcp/api/v1/tools/local` | +| Tool 직접 실행 | `POST` | `/mcp/{toolName}` | +| Tool Manifest 조회 | `GET` | `/tool-manifest` | -| 목적 | 메서드 | 경로 | 구현 | -|---|---|---|---| -| 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` 목록을 반환합니다. +`GET /tool-manifest`는 `If-None-Match` 요청 헤더를 지원합니다. Manifest가 변경되지 않았으면 `304 Not Modified`를 반환합니다. ### REST 실행 예시 -Tool 이름은 `@McpFunction.name` 값입니다. 예를 들어 OTH Pod의 `oth.smp.weather.inquiry`는 다음처럼 호출합니다. +다음은 SYS Pod의 시스템 상태 Tool 호출 예시입니다. ```powershell $headers = @{ @@ -57,212 +85,221 @@ $headers = @{ Invoke-RestMethod ` -Method Post ` - -Uri 'http://localhost:8084/mcp/oth.smp.weather.inquiry' ` + -Uri 'http://localhost:8086/mcp/iam_system_status' ` -Headers $headers ` -ContentType 'application/json' ` - -Body '{"city":"Seoul"}' + -Body '{"environment":"개발"}' ``` -Controller는 이름을 찾은 뒤 요청 JSON을 첫 번째 DTO 매개변수로 변환합니다. 입력 Schema 검증 실패는 `422 INVALID_PARAM`, 존재하지 않는 Tool은 `404 TOOL_NOT_FOUND`, 실행 예외는 `502 TOOL_ERROR` 응답입니다. `trace-id`와 `request-id`는 성공 응답 헤더로 다시 전달됩니다. +주요 실행 응답은 다음과 같습니다. -## Tool 검색과 MCP 노출 규칙 +| HTTP 상태 | 코드 | 의미 | +|---:|---|---| +| 200 | - | Tool 실행 성공 | +| 404 | `TOOL_NOT_FOUND` | 요청한 Tool 이름이 없음 | +| 422 | `INVALID_PARAM` | 요청이 입력 Schema와 일치하지 않음 | +| 500 | `INVALID_TOOL_RESPONSE` | 결과가 출력 Schema와 일치하지 않음 | +| 502 | `TOOL_ERROR` | Tool 실행 중 예외 발생 | -애플리케이션 기동 시 `LocalToolScanner`는 Spring Bean에서 `@McpTool`과 `@McpFunction` 메타데이터를 읽어 로컬 Tool 목록을 만듭니다. 기동 완료 후 `ToolPodMcpToolSynchronizer`는 이 목록 중 `visible = true`인 Tool만 MCP SDK 서버에 추가합니다. +## Tool 구현 방식 -| 속성 | 현재 구현에서의 의미 | -|---|---| -| `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으로 구성되어야 합니다. +호출 가능한 메서드는 Spring AI Community의 `@McpTool`로 선언합니다. 프로젝트 고유 실행·표시 정보는 `@GrowToolHint`로 보완합니다. ```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); -} +@McpTool( + name = "iam_system_status", + title = "시스템 상태 조회", + description = "모의 시스템 상태 정보를 조회합니다.", + annotations = @McpTool.McpAnnotations(openWorldHint = false) +) +@GrowToolHint( + categoryKey = "iam", + mappingId = "DIRECT_IAM_STATUS", + requiresApproval = false +) +SystemStatusResponse getSystemStatus(SystemStatusRequest 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의 청구 검색 예제는 다음 리소스를 사용합니다. +`@McpTool.name`은 다음 형식을 사용합니다. ```text -dap-was-oth/src/main/resources/tool-schemas/cmm/ -├─ claim-search-resource-input-schema.json -└─ claim-search-resource-output-schema.json +^[a-z][a-z0-9_]{2,63}$ ``` -입력 검증에는 JSON Schema Draft 7이 사용됩니다. 출력 Schema 검증에 실패하면 `500 INVALID_TOOL_RESPONSE`을 반환합니다. +예: `cmm_claim_search`, `crm_customer_detail`, `iam_system_status` + +이름 중복과 형식은 `validateMcpToolNames`, V17 정의는 `validateToolSchemaV17` Gradle 작업으로 검사합니다. 모든 `bootJar` 작업은 두 검증 작업에 의존합니다. + +## Tool YAML 정의 + +각 Tool은 업무 모듈의 다음 경로에 YAML 정의를 가집니다. + +```text +dap-was-*/src/main/resources/tool-definitions/{category}/{tool-name}.yml +``` + +V17 정의의 주요 필수 항목은 다음과 같습니다. + +- `name`, `display_name`, `version`, `category_key` +- `description.function`, `when_to_use`, `when_not_to_use`, `io_limits` +- 3~10개의 `example_queries` +- `read_only`, `destructive`, `idempotent` +- `parameters_schema.type: object` +- `parameters_schema.additionalProperties: false` +- 각 입력 property의 `description` + +입력·출력 Schema는 `ToolSchemaResolver`가 어노테이션의 Schema 리소스와 인라인 Schema, DTO에서 생성한 Schema를 해석합니다. `ToolRegistryHeartbeatSender`는 여기에 YAML의 `parameters_schema`와 `output_schema`를 병합하여 최종 메타데이터를 만듭니다. ## 로컬 실행 ### 사전 조건 - JDK 21 -- Docker (Redis 또는 MCI mock을 사용할 경우) -- Gradle Wrapper 사용 권장 +- 프로젝트에 포함된 Gradle Wrapper +- Redis 또는 외부 연동이 필요한 경우 Docker -로컬 프로필은 기본값이며, 두 Pod 모두 H2 메모리 DB와 P6Spy를 설정합니다. Pod URL은 `AXHUB_TOOL_URL` 환경 변수로 설정하며, 지정하지 않으면 해당 `server.port`의 localhost 주소를 사용합니다. +기본 활성 프로필은 `local`입니다. 로컬 프로필은 H2 메모리 DB와 P6Spy를 사용합니다. ```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를 확인할 수 있습니다. +각 Pod는 별도 터미널에서 실행합니다. + +```powershell +# 영업 Tool Pod +$env:AXHUB_TOOL_URL = 'http://localhost:8082' +.\gradlew.bat :dap-was-sal:bootRun + +# 고객 Tool Pod +$env:AXHUB_TOOL_URL = 'http://localhost:8084' +.\gradlew.bat :dap-was-cus:bootRun + +# 상품 Tool Pod +$env:AXHUB_TOOL_URL = 'http://localhost:8085' +.\gradlew.bat :dap-was-pro:bootRun + +# 시스템 Tool Pod +$env:AXHUB_TOOL_URL = 'http://localhost:8086' +.\gradlew.bat :dap-was-sys:bootRun +``` + +실행 후 SYS Pod 기준 확인 URL은 다음과 같습니다. ```text -http://localhost:8084/mcp/api/v1/tools/local -http://localhost:8084/tool-manifest -http://localhost:8084/tool-test-console.html +http://localhost:8086/mcp/api/v1/tools/local +http://localhost:8086/tool-manifest +http://localhost:8086/swagger-ui/index.html ``` -`tool-test-console.html`은 공통 라이브러리의 정적 리소스입니다. `/tool-manifest`에서 Tool과 입력 Schema를 읽어 요청 JSON을 만들고, 현재 Pod의 `/mcp/{toolName}`으로 호출합니다. 저장한 테스트 케이스는 브라우저 `localStorage`에 보관됩니다. +### 주요 환경 변수 -## 테스트와 빌드 +| 환경 변수 | 설명 | 기본값 | +|---|---|---| +| `SPRING_PROFILES_ACTIVE` | Spring 활성 프로필 | `local` | +| `PORT` | Pod 수신 포트 | 모듈별 기본 포트 | +| `AXHUB_TOOL_URL` | Manifest와 Gateway 등록에 사용할 Pod 외부 URL | `http://localhost:${server.port}` | +| `AXHUB_GATEWAY_URL` | 외부 Gateway URL | `http://localhost:8081` | +| `SPRING_DATA_REDIS_HOST` | Redis 호스트 | `localhost` | +| `SPRING_DATA_REDIS_PORT` | Redis 포트 | `6379` | +| `GLOW_COMMUNICATION_MCI_HOST` | MCI 대상 호스트 | 프로필별 설정 | +| `GLOW_COMMUNICATION_MCI_PORT` | MCI 대상 포트 | 프로필별 설정 | + +## 테스트와 검증 ```powershell +# 운영 소스 전체 컴파일 +.\gradlew.bat classes + # 전체 테스트 .\gradlew.bat test -# 공통 라이브러리 테스트 +# 모듈별 테스트 .\gradlew.bat :dap-was-lib:test +.\gradlew.bat :dap-was-cus:test +.\gradlew.bat :dap-was-sal:test +.\gradlew.bat :dap-was-pro:test +.\gradlew.bat :dap-was-sys:test -# OTH Tool 테스트 -.\gradlew.bat :dap-was-oth:test - -# Tool 이름 규칙 및 중복 검증 +# Tool 이름·중복 검사 .\gradlew.bat validateMcpToolNames -# 패키징 전 전체 빌드 +# 203개 Tool V17 정의 검사 +.\gradlew.bat validateToolSchemaV17 + +# 검증, 테스트, 패키징 .\gradlew.bat clean build ``` -테스트는 공통 MCP Schema/Manifest/Header 처리, Glow MCI 파서, Tool 이름 검증과 OTH의 청구·SOL·MCI 변환을 다룹니다. SMS 모듈에는 현재 별도 테스트 소스가 없습니다. +### 현재 검증 상태 + +2026-08-18 기준 확인 결과입니다. + +- 전체 운영 소스 `classes`: 성공 +- `validateMcpToolNames`: 성공 +- `validateToolSchemaV17`: 203개 Tool 성공 +- SAL·PRO·SYS 테스트: 20개 성공 +- 전체 `test`: 테스트 소스 컴파일 오류로 실패 + - LIB의 `ToolScaffolderTest`가 변경 전 `FieldDefinition` 생성자를 사용 + - LIB의 `ToolManifestServiceTest` 패키지와 테스트용 생성자 접근 범위가 불일치 + +전체 빌드의 기준을 회복하려면 위 테스트 소스 회귀를 먼저 정리해야 합니다. ## Docker Compose -현재 Compose 서비스와 호스트 포트는 다음과 같습니다. +Compose는 네 업무 Pod를 정의합니다. | 서비스 | 컨테이너 포트 | 호스트 포트 | |---|---:|---:| -| `redis` | 6379 | 6379 | -| `was-sms` | 8082 | 8282 | -| `was-oth` | 8084 | 8284 | +| `was-sal` | 8082 | 8282 | +| `was-cus` | 8084 | 8284 | +| `was-pro` | 8085 | 8285 | +| `was-sys` | 8086 | 8286 | -Compose의 Pod URL은 컨테이너 DNS 이름을 사용합니다. +모듈 Dockerfile은 사전에 생성된 Boot JAR를 이미지에 복사합니다. 먼저 JAR를 빌드한 뒤 Compose를 실행합니다. -```text -was-sms: http://was-sms:8082 -was-oth: http://was-oth:8084 +```powershell +.\gradlew.bat :dap-was-sal:bootJar :dap-was-cus:bootJar :dap-was-pro:bootJar :dap-was-sys:bootJar +docker compose up --build ``` -### Docker 컨테이너 기동 +주의: `docker-compose.yml`은 `http://gateway:8081`을 Gateway 주소로 사용하지만 이 저장소의 Compose에는 `gateway` 서비스가 없습니다. 동일 Docker 네트워크에 Gateway를 제공하거나 `AXHUB_GATEWAY_URL`을 실제 접근 가능한 주소로 변경해야 합니다. -별도의 CI/CD 러너나 외부 의존성(MCI Mock 등) 없이 독립적으로 실행 가능하도록 구성되어 있습니다. `docker-compose.yml`을 통해 Redis 및 각 Pod 컨테이너를 구동할 수 있습니다. +## 보안 및 운영 주의사항 +현재 구현을 운영 환경에 노출하기 전에 아래 항목을 반드시 점검해야 합니다. -## 설정 +- API Key 인터셉터는 `/rpc/**`, `/mcp/api/v1/**`에만 적용됩니다. 실제 Tool 실행 경로인 `/mcp/{toolName}`과 MCP 전송 경로 `/mcp`는 현재 검사 대상이 아닙니다. +- `mcp.security.api-keys`가 비어 있으면 인터셉터가 익명 요청을 허용합니다. 현재 기본 설정에는 API Key가 정의되어 있지 않습니다. +- `mcp.security.tenant-domains`는 설정 객체에 바인딩되지만 Tool별 인가에 사용되지 않습니다. +- `requiresApproval`은 메타데이터에만 기록되며 실행 차단이나 승인 확인 로직은 없습니다. +- 입력·출력 Schema 처리 자체에서 예외가 발생하면 현재 실행 서비스는 로그를 남기고 검증을 건너뜁니다. +- CORS는 모든 Origin을 허용하면서 credential도 허용하도록 설정되어 있습니다. 운영 Origin을 명시적으로 제한해야 합니다. +- Gateway 등록은 별도 비관리 스레드에서 Tool별로 최대 12회 재시도합니다. Gateway 장애 시 장시간 실행될 수 있으므로 타임아웃과 종료 정책을 점검해야 합니다. +- Tool 요청과 연동 오류 로그에 개인정보나 인증정보가 포함되지 않도록 DTO와 로그 마스킹 정책을 검토해야 합니다. -| 설정 | 위치/환경 변수 | 설명 | -|---|---|---| -| 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` | +| 공통 빌드 및 검증 작업 | `build.gradle` | +| REST Tool 실행 API | `dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java` | +| Tool 실행 서비스 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java` | +| 실행 메서드 Registry | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistry.java` | +| MCP SDK 서버 | `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` | +| Tool 스캔 및 Gateway 등록 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java` | +| YAML Tool 정의 로딩 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/metadata/ToolDefinitionRepository.java` | +| V17 정의 검증 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/metadata/ToolDefinitionValidator.java` | +| Manifest 생성 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java` | +| API Key 인터셉터 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java` | +| Tool·Pod 스캐폴딩 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java`, `PodScaffolder.java` | + +## 개발 시 권장 확인 순서 + +1. Tool 메서드와 요청·응답 DTO를 구현합니다. +2. 동일 이름의 V17 YAML 정의를 `tool-definitions` 아래에 추가합니다. +3. `validateMcpToolNames`와 `validateToolSchemaV17`을 실행합니다. +4. 모듈 테스트와 전체 테스트를 실행합니다. +5. 로컬 Pod에서 `/mcp/api/v1/tools/local`과 `/tool-manifest`를 확인합니다. +6. REST와 MCP 양쪽에서 동일한 Tool 결과와 오류 계약을 확인합니다. diff --git a/dap-was-cus/src/test/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadControllerTest.java b/dap-was-cus/src/test/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadControllerTest.java deleted file mode 100644 index be9f97ee6..000000000 --- a/dap-was-cus/src/test/java/io/shinhanlife/dap/mcc/presentation/DtoExcelDownloadControllerTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.shinhanlife.dap.mcc.presentation; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.ByteArrayInputStream; - -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.junit.jupiter.api.Test; -import org.springframework.http.ResponseEntity; - -class DtoExcelDownloadControllerTest { - - private final DtoExcelDownloadController controller = new DtoExcelDownloadController(); - - @Test - void downloadsBothOnild0320Variants() throws Exception { - assertWorkbook("ONILD0320_I", "csNo"); - assertWorkbook("ONILD0320_O", "notiDt"); - } - - private void assertWorkbook(String dtoName, String expectedField) throws Exception { - ResponseEntity response = controller.download(dtoName); - - assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); - assertThat(response.getBody()).isNotNull(); - try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(response.getBody()))) { - assertThat(workbook.getSheetAt(0)) - .anySatisfy(row -> assertThat(row) - .anySatisfy(cell -> assertThat(cell.toString()).isEqualTo(expectedField))); - } - } -}