forked from kimhyungsik/ax_hub_mcp_tool
306 lines
13 KiB
Markdown
306 lines
13 KiB
Markdown
# DAP WAS MCP Tool Pods
|
|
|
|
신한라이프 업무 기능을 MCP(Model Context Protocol) Tool로 제공하는 Java 멀티 모듈 프로젝트입니다. 각 업무 모듈은 독립 실행 가능한 Spring Boot 애플리케이션이며, MCP Streamable HTTP와 REST 실행 API를 함께 제공합니다.
|
|
|
|
이 저장소에는 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 또는 REST Client
|
|
├─ MCP Streamable HTTP: /mcp
|
|
└─ REST: POST /mcp/{toolName}
|
|
│
|
|
▼
|
|
Tool Pod
|
|
├─ McpToolMethodRegistry
|
|
│ └─ Spring Bean의 @McpTool 메서드 탐색 및 실행 메서드 캐시
|
|
├─ ToolRegistryHeartbeatSender
|
|
│ ├─ Tool 메타데이터 생성
|
|
│ ├─ tool-definitions YAML 병합
|
|
│ └─ 외부 Gateway 등록 시도
|
|
├─ McpToolExecutionService
|
|
│ ├─ 입력 Schema 검증
|
|
│ ├─ 요청 DTO 변환 및 Tool 호출
|
|
│ └─ 출력 Schema 검증
|
|
└─ UseCase → Converter → MCI/EAI/HTTP Client 또는 Mock 응답
|
|
```
|
|
|
|
애플리케이션 시작 시 다음 순서로 Tool이 준비됩니다.
|
|
|
|
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`를 통해 실행됩니다.
|
|
|
|
## 제공 API
|
|
|
|
각 업무 Pod가 동일한 API 구조를 제공합니다.
|
|
|
|
| 목적 | 메서드 | 경로 |
|
|
|---|---|---|
|
|
| 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` |
|
|
|
|
`GET /tool-manifest`는 `If-None-Match` 요청 헤더를 지원합니다. Manifest가 변경되지 않았으면 `304 Not Modified`를 반환합니다.
|
|
|
|
### REST 실행 예시
|
|
|
|
다음은 SYS Pod의 시스템 상태 Tool 호출 예시입니다.
|
|
|
|
```powershell
|
|
$headers = @{
|
|
'trace-id' = 'trace-local-001'
|
|
'request-id' = 'request-local-001'
|
|
}
|
|
|
|
Invoke-RestMethod `
|
|
-Method Post `
|
|
-Uri 'http://localhost:8086/mcp/iam_system_status' `
|
|
-Headers $headers `
|
|
-ContentType 'application/json' `
|
|
-Body '{"environment":"개발"}'
|
|
```
|
|
|
|
주요 실행 응답은 다음과 같습니다.
|
|
|
|
| HTTP 상태 | 코드 | 의미 |
|
|
|---:|---|---|
|
|
| 200 | - | Tool 실행 성공 |
|
|
| 404 | `TOOL_NOT_FOUND` | 요청한 Tool 이름이 없음 |
|
|
| 422 | `INVALID_PARAM` | 요청이 입력 Schema와 일치하지 않음 |
|
|
| 500 | `INVALID_TOOL_RESPONSE` | 결과가 출력 Schema와 일치하지 않음 |
|
|
| 502 | `TOOL_ERROR` | Tool 실행 중 예외 발생 |
|
|
|
|
## Tool 구현 방식
|
|
|
|
호출 가능한 메서드는 Spring AI Community의 `@McpTool`로 선언합니다. 프로젝트 고유 실행·표시 정보는 `@GrowToolHint`로 보완합니다.
|
|
|
|
```java
|
|
@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);
|
|
```
|
|
|
|
`@McpTool.name`은 다음 형식을 사용합니다.
|
|
|
|
```text
|
|
^[a-z][a-z0-9_]{2,63}$
|
|
```
|
|
|
|
예: `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
|
|
- 프로젝트에 포함된 Gradle Wrapper
|
|
- Redis 또는 외부 연동이 필요한 경우 Docker
|
|
|
|
기본 활성 프로필은 `local`입니다. 로컬 프로필은 H2 메모리 DB와 P6Spy를 사용합니다.
|
|
|
|
```powershell
|
|
$env:SPRING_PROFILES_ACTIVE = 'local'
|
|
```
|
|
|
|
각 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:8086/mcp/api/v1/tools/local
|
|
http://localhost:8086/tool-manifest
|
|
http://localhost:8086/swagger-ui/index.html
|
|
```
|
|
|
|
### 주요 환경 변수
|
|
|
|
| 환경 변수 | 설명 | 기본값 |
|
|
|---|---|---|
|
|
| `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
|
|
|
|
# Tool 이름·중복 검사
|
|
.\gradlew.bat validateMcpToolNames
|
|
|
|
# 203개 Tool V17 정의 검사
|
|
.\gradlew.bat validateToolSchemaV17
|
|
|
|
# 검증, 테스트, 패키징
|
|
.\gradlew.bat clean build
|
|
```
|
|
|
|
### 현재 검증 상태
|
|
|
|
2026-08-18 기준 확인 결과입니다.
|
|
|
|
- 전체 운영 소스 `classes`: 성공
|
|
- `validateMcpToolNames`: 성공
|
|
- `validateToolSchemaV17`: 203개 Tool 성공
|
|
- SAL·PRO·SYS 테스트: 20개 성공
|
|
- 전체 `test`: 테스트 소스 컴파일 오류로 실패
|
|
- LIB의 `ToolScaffolderTest`가 변경 전 `FieldDefinition` 생성자를 사용
|
|
- LIB의 `ToolManifestServiceTest` 패키지와 테스트용 생성자 접근 범위가 불일치
|
|
|
|
전체 빌드의 기준을 회복하려면 위 테스트 소스 회귀를 먼저 정리해야 합니다.
|
|
|
|
## Docker Compose
|
|
|
|
Compose는 네 업무 Pod를 정의합니다.
|
|
|
|
| 서비스 | 컨테이너 포트 | 호스트 포트 |
|
|
|---|---:|---:|
|
|
| `was-sal` | 8082 | 8282 |
|
|
| `was-cus` | 8084 | 8284 |
|
|
| `was-pro` | 8085 | 8285 |
|
|
| `was-sys` | 8086 | 8286 |
|
|
|
|
모듈 Dockerfile은 사전에 생성된 Boot JAR를 이미지에 복사합니다. 먼저 JAR를 빌드한 뒤 Compose를 실행합니다.
|
|
|
|
```powershell
|
|
.\gradlew.bat :dap-was-sal:bootJar :dap-was-cus:bootJar :dap-was-pro:bootJar :dap-was-sys:bootJar
|
|
docker compose up --build
|
|
```
|
|
|
|
주의: `docker-compose.yml`은 `http://gateway:8081`을 Gateway 주소로 사용하지만 이 저장소의 Compose에는 `gateway` 서비스가 없습니다. 동일 Docker 네트워크에 Gateway를 제공하거나 `AXHUB_GATEWAY_URL`을 실제 접근 가능한 주소로 변경해야 합니다.
|
|
|
|
## 보안 및 운영 주의사항
|
|
|
|
현재 구현을 운영 환경에 노출하기 전에 아래 항목을 반드시 점검해야 합니다.
|
|
|
|
- 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와 로그 마스킹 정책을 검토해야 합니다.
|
|
|
|
## 주요 소스 위치
|
|
|
|
| 주제 | 위치 |
|
|
|---|---|
|
|
| 공통 빌드 및 검증 작업 | `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 스캔 및 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 결과와 오류 계약을 확인합니다.
|