diff --git a/README.md b/README.md index 650cddc6..5306e961 100644 --- a/README.md +++ b/README.md @@ -1,606 +1,271 @@ -# AX HUB MCP Tool +# AX HUB MCP Tool Platform -신한라이프 업무 시스템과 AI Agent를 연결하는 MCP(Model Context Protocol) Gateway 및 Tool 서버 프로젝트입니다. +AX HUB에서 AI Agent가 업무 Tool을 검색하고 호출할 수 있도록 Gateway와 독립 Tool Pod를 제공하는 멀티 모듈 Spring Boot 프로젝트입니다. -Agent는 Gateway에서 Tool 목록과 입력 명세를 받고, Gateway는 권한과 정책을 확인한 뒤 Tool 서버로 요청을 전달합니다. 업무 Tool은 `DTO → UseCase → Converter → MCI/EAI Client` 구조로 레거시 시스템을 호출합니다. - -## 전체 흐름 +## 1. 현재 구성 ```text -AI Agent / MCP Client - │ - ▼ -MCP Gateway (dap-gateway) - ├─ Tool 목록·스키마 제공 - ├─ Tool 권한·승인·가드레일 확인 - ├─ Redis Registry 및 실행 추적 - └─ 대상 Tool 서버로 라우팅 - │ - ▼ -Tool Server (dap-tool-sms / dap-tool-oth) - └─ BusinessToolController - │ - ▼ -UseCase → Converter → MCI/EAI Client → 레거시 시스템 +MCP Client / AI Agent + | + v +Gateway (dap-gateway, 8081) + - MCP 연결, Tool Registry, 인증·인가, 라우팅, 공통 화면 + | + +-------------------------+ + | | + v v +SMS Tool Pod (dap-was-sms, 8082) OTH Tool Pod (dap-was-oth, 8084) + - SMS 업무 Tool - 공통/업무/MCI 연계 Tool + - Tool Manifest - Tool Manifest + - Pod Test Console - Pod Test Console + | + v +MCI / EAI / EIMS 등 레거시 연계 대상 ``` -## 현재 구조와 목표 구조 +Gateway는 외부 MCP 진입점과 Tool 등록·라우팅을 담당하고, 실제 업무 로직과 레거시 호출은 각 Tool Pod가 담당합니다. Tool Pod는 업무/조직 단위로 독립 배포할 수 있습니다. -현재는 Gateway와 두 개의 Tool 애플리케이션으로 구성됩니다. +## 2. 모듈 -```text -현재: Gateway + SMS Tool Pod + OTH Tool Pod -목표: Gateway + 고객 Pod + 영업 Pod + 지급/납입 Pod + 알림 Pod + 인사 Pod + 공통 Pod -``` - -`dap-tool-oth`에는 여러 업무 카테고리가 함께 있습니다. AA 협의 후에는 부서·업무 소유권 단위로 Tool 서버, 이미지, Pod, 배포 파이프라인을 분리합니다. 이 목표 구조는 향후 전환 방향이며 현재 구현 완료 상태가 아닙니다. - -## Gradle 멀티모듈 - -| 모듈 | 역할 | 실행 포트 | +| 모듈 | 역할 | 기본 포트 | |---|---|---:| -| `dap-gateway` | MCP 진입점, Tool Registry, 라우팅, 권한·가드레일, Chat API | 8081 | -| `dap-tool-core` | 공통 어노테이션, Controller, JSON Schema, MCI/EAI 지원, 보안·로깅 공통 기능 | 라이브러리 | -| `dap-tool-sms` | SMS/알림 Tool 서버 | 8082 | -| `dap-tool-oth` | 공통·업무·샘플·MCI Tool 서버 | 8084 | +| `dap-gateway` | MCP Gateway, Registry, 라우팅, Chat·Catalog·Tester·Scaffold 화면 | 8081 | +| `dap-was-lib` | Tool 공통 라이브러리: 어노테이션, Schema, Manifest, MCI/EAI 어댑터, 공통 Web·보안 기능 | - | +| `dap-was-sms` | SMS/알림 업무 Tool Pod | 8082 | +| `dap-was-oth` | 공통/샘플/MCI 연계 업무 Tool Pod | 8084 | -기술 기준은 Java 21, Spring Boot 4, Gradle, Spring AI MCP Server, Redis, MapStruct, MyBatis, Resilience4j입니다. +> 기존 `dap-tool-core`, `dap-tool-sms`, `dap-tool-oth` 명칭은 현재 각각 `dap-was-lib`, `dap-was-sms`, `dap-was-oth`로 변경되었습니다. -## 환경 (Environment) +## 3. 개발 환경 -| 항목 | 버전 / 기준 | +| 항목 | 기준 | |---|---| | Java | 21 | | Spring Boot | 4.0.5 | -| Gradle Wrapper | 8.14.3 | +| Gradle | 8.14.3 (Wrapper) | | Spring AI BOM | 2.0.0 | -| Spring AI MCP Server | `spring-ai-starter-mcp-server-webmvc` | -| Redis Client | Lettuce 6.6.0.RELEASE | -| Resilience | Resilience4j 2.2.0 | -| MyBatis Spring Boot Starter | 3.0.3 | -| MapStruct | 1.5.5.Final | -| Lombok | 1.18.32 | -| JSON Schema Validator | networknt 1.4.0 | -| OpenAPI UI | springdoc 2.5.0 | -| 컨테이너 실행 | Docker Compose | +| 기본 프로필 | `local` | -프로젝트는 JDK 21을 기준으로 컴파일됩니다. IntelliJ에서는 Project SDK, Gradle JVM, Run Configuration JRE를 모두 JDK 21로 맞춰야 합니다. -## 5분 빠른 시작 - -Docker와 JDK 21이 준비된 로컬 개발 환경 기준입니다. +로컬 환경에서는 민감한 값은 환경 변수로만 주입합니다. ```powershell -# 1. Redis와 MCI Mock만 먼저 실행 -$env:ACTIVE_PROFILE = 'local' -docker compose up -d redis mci-mock - -# 2. Gateway 실행 (새 PowerShell) $env:SPRING_PROFILES_ACTIVE = 'local' -$env:OPENROUTER_API_KEY = '<개발용 비밀 저장소의 키>' +$env:OPENROUTER_API_KEY = '<발급받은-키>' +``` + +## 4. 실행 + +### 4.1 Gradle 로컬 실행 + +각 애플리케이션은 별도 터미널에서 실행합니다. + +```powershell .\gradlew.bat :dap-gateway:bootRun - -# 3. OTH Tool 실행 (또 다른 PowerShell) -$env:SPRING_PROFILES_ACTIVE = 'local' -$env:AXHUB_GATEWAY_URL = 'http://localhost:8081' -$env:AXHUB_TOOL_URL = 'http://localhost:8084' -.\gradlew.bat :dap-tool-oth:bootRun +.\gradlew.bat :dap-was-sms:bootRun +.\gradlew.bat :dap-was-oth:bootRun ``` -Tool 서버가 기동된 뒤 아래 URL로 등록된 Tool 목록을 확인합니다. - -```text -http://localhost:8081/mcp/api/v1/tools/list -``` - -SMS Tool도 함께 확인하려면 별도 PowerShell에서 아래 명령을 실행합니다. +### 4.2 Docker Compose 실행 ```powershell -$env:SPRING_PROFILES_ACTIVE = 'local' -$env:AXHUB_GATEWAY_URL = 'http://localhost:8081' -$env:AXHUB_TOOL_URL = 'http://localhost:8082' -.\gradlew.bat :dap-tool-sms:bootRun -``` - -전체 컨테이너 환경이 필요하면 개별 실행 대신 다음 한 줄을 사용합니다. - -```powershell -$env:ACTIVE_PROFILE = 'local' docker compose up -d --build ``` -## Tool 등록과 실행 -### 등록과 목록 제공 +| 서비스 | 호스트 URL | 컨테이너 포트 | +|---|---|---:| +| Gateway | http://localhost:8281 | 8081 | +| SMS Tool Pod | http://localhost:8282 | 8082 | +| OTH Tool Pod | http://localhost:8284 | 8084 | +| MCI Mock | http://localhost:8089 | 8080 | +| Dozzle 로그 화면 | http://localhost:8288 | 8080 | -1. Tool 서버 기동 시 `ToolRegistryHeartbeatSender`가 `@McpTool`, `@McpFunction`을 스캔합니다. -2. Tool 이름, 설명, 입력 JSON Schema, `categoryKey`, 연동 방식, 실행 URL을 메타데이터로 생성합니다. -3. Gateway Redis Registry에 등록·Heartbeat 정보를 전송합니다. -4. Agent와 관리 화면은 Gateway에서 Tool 목록과 명세를 조회합니다. +## 5. 화면과 운영 도구 -### 실행 +### Gateway 화면 -1. Agent가 Gateway에 Tool 이름과 입력값을 보냅니다. -2. Gateway가 Tool 존재 여부, 허용 Tool, 쓰기 승인, 가드레일을 확인합니다. -3. Gateway가 Tool 서버의 `/mcp/{toolName}`으로 요청을 전달합니다. -4. `BusinessToolController`가 Tool 메서드를 찾아 DTO로 변환하고 JSON Schema를 검증합니다. -5. UseCase가 업무 흐름을 수행합니다. -6. Converter가 업무 DTO를 인터페이스 ID 기반 MCI 요청 DTO로 변환합니다. -7. MCI/EAI Client가 레거시를 호출하고 결과를 Tool 응답으로 반환합니다. - -## 주요 URL - -로컬에서 Gateway를 직접 실행할 때의 기준입니다. Docker Compose를 사용하면 Gateway 호스트 포트는 `8281`입니다. - -| 용도 | 메서드 | URL | +| 화면 | 로컬 URL | 용도 | |---|---|---| -| Tool 목록 | `GET` | `http://localhost:8081/mcp/api/v1/tools/list` | -| Tool 실행 | `POST` | `http://localhost:8081/mcp/api/v1/tools/call` | -| Tool Markdown 문서 | `GET` | `http://localhost:8081/mcp/api/v1/tools/docs/markdown` | -| 카테고리별 SSE MCP 연결 | `GET` | `http://localhost:8081/mcp/sse/{categoryKey}` | -| 카테고리별 MCP 호출 채널 | `POST` | `http://localhost:8081/mcp/custom/{categoryKey}` | -| Chat 스트리밍 API | `POST` | `http://localhost:8081/api/chat` | -| Scaffold API | `POST` | `http://localhost:8081/api/v1/scaffold/pod` 또는 `/tool` | +| 메인 | http://localhost:8081/index.html | 플랫폼 진입 화면 | +| Catalog | http://localhost:8081/catalog.html | 등록 Tool 탐색 | +| Playground | http://localhost:8081/playground.html | 단건 Tool 호출 확인 | +| Chat | http://localhost:8081/chat.html | Agent 기반 대화형 호출 | +| Auto Tester | http://localhost:8081/tester.html | Gateway 기준 다중 Tool 스모크/회귀 테스트 | +| Scaffold | http://localhost:8081/admin/scaffold.html | Pod·Tool 소스 생성 지원 | -`/mcp/sse/{categoryKey}`는 SSE 연결을 여는 전송 경로이고, `/mcp/custom/{categoryKey}`는 같은 카테고리의 MCP 요청을 처리하는 호출 경로입니다. 두 URL은 역할이 다릅니다. +Docker Compose로 실행한 경우 Gateway 화면은 포트 `8281`을 사용합니다. -## Tool 개발 표준 +### Tool Pod Test Console -| 구성 요소 | 책임 | -|---|---| -| `XxxRequest`, `XxxResponse` | Agent/Tool 관점의 입력·응답 DTO | -| `XxxUseCase` | Tool 계약과 MCP 메타데이터 선언 | -| `XxxUseCaseImpl` | 업무 흐름 조합과 Client 호출 | -| `XxxConverter` | 업무 DTO와 레거시 인터페이스 DTO 사이 변환 | -| `MciXxxClient` | Glow/MCI 또는 EAI 통신 호출 | -| `INTERFACE_ID_I`, `INTERFACE_ID_O` | 인터페이스 ID 기준 MCI 요청·응답 DTO | +공통 정적 화면인 `tool-test-console.html`은 Tool Pod의 `/tool-manifest`를 읽어, 해당 Pod에 등록된 Tool과 `inputSchema`를 기준으로 요청 JSON을 만들어 직접 실행합니다. -### Tool 선언 - -Tool 그룹에는 `@McpTool`, Agent가 호출하는 함수에는 `@McpFunction`을 사용합니다. - -```java -@McpTool(routingType = "MCI", categoryKey = "claim") -public interface ClaimInquiryUseCase { - - @McpFunction( - name = "claim_inquiry", - displayName = "보험금 청구 조회", - description = "청구 번호로 보험금 청구 상태를 조회합니다.", - mappingId = "CLM00000001" - ) - ClaimInquiryResponse inquire(ClaimInquiryRequest request); -} -``` - -`categoryKey`는 Tool의 업무 그룹입니다. Gateway의 목록 필터링, Agent 권한, 동적 MCP 서버 구분에 사용하므로 합의된 업무 키를 사용합니다. - -- `MCI`: 사내 MCI 인터페이스 호출 -- `EAI`: EAI 연동 -- `DIRECT`: 외부 HTTP 또는 내부 직접 연동 - -### 변환 원칙 - -UseCase 구현체는 Tool 요청을 레거시 요청과 섞어 쓰지 않습니다. Converter에서 변환한 뒤 Client에 전달합니다. - -```java -@Override -public ClaimInquiryResponse inquire(ClaimInquiryRequest request) { - CLM00000001_I mciRequest = converter.toMciRequest(request); - CLM00000001_O mciResponse = mciClmClient.callClm00000001(mciRequest); - return converter.toResponse(mciResponse); -} -``` - -MCI 입출력 객체는 업무 이름이 아니라 인터페이스 ID를 기준으로 둡니다. - -```text -CLCNNB00001_I : CLCNNB00001 요청 DTO -CLCNNB00001_O : CLCNNB00001 응답 DTO -``` - -예를 들어 `Onnba3011Request`와 `CLCNNB00001_I`는 같은 업무 데이터를 담을 수 있지만 같은 객체가 아닙니다. 둘 사이의 변환 책임은 `Onnba3011Converter`에 둡니다. - -### 새 Tool 추가 체크리스트 - -새 업무 Tool을 추가할 때는 아래 순서로 확인합니다. - -- [ ] 소속 모듈과 `categoryKey`를 업무 소유 조직 기준으로 결정한다. -- [ ] Agent 입력·응답 DTO인 `XxxRequest`, `XxxResponse`를 만든다. -- [ ] `XxxUseCase`에 `@McpTool`을 선언하고, 호출 메서드에 `@McpFunction`의 이름·설명·연동 ID를 선언한다. -- [ ] `XxxUseCaseImpl`에서 업무 흐름만 조합한다. -- [ ] `XxxConverter`에 업무 DTO ↔ 인터페이스 ID DTO 변환을 둔다. -- [ ] `MciXxxClient`와 `INTERFACE_ID_I`, `INTERFACE_ID_O`를 인터페이스 ID 기준으로 만든다. -- [ ] DTO에 Bean Validation을 선언하고, 중첩 DTO가 있으면 입력 Schema와 검증 대상에 포함되는지 확인한다. -- [ ] 조회·변경 작업 특성에 따라 `readOnlyHint`, `requiresApproval`, `idempotentHint`를 설정한다. -- [ ] 단위 테스트를 작성하고 `:dap-tool-core:test` 또는 대상 모듈 테스트를 실행한다. -- [ ] Tool 서버 기동 후 `/mcp/api/v1/tools/list`에서 이름, 설명, category, JSON Schema가 맞는지 확인한다. -- [ ] 요청·응답 로그에 개인정보나 인증값이 남지 않는지 확인한다. -### 등록·노출 제어 - -| 속성 | 의미 | -|---|---| -| `register` | Redis Registry와 Gateway 카탈로그에 등록할지 여부 | -| `visible` | Agent/클라이언트 목록에 표시할지 여부 | -| `requiresApproval` | 쓰기·고위험 작업의 승인 요구 여부 | -| `readOnlyHint`, `destructiveHint`, `idempotentHint` | Agent 호출 특성 힌트 | - -`register = false`인 Tool은 자동 카탈로그 등록 대상이 아닙니다. 별도 실행 목적이 있는 경우에만 사용하고, 필요한 fallback 경로를 운영 설정으로 확인합니다. - -## 빌드·테스트·로컬 실행 - -### 사전 조건 - -- JDK 21 -- Gradle Wrapper 사용 권장 -- 로컬 Redis 또는 Docker Compose 환경 -- 필요 시 MCI Mock 또는 사내 MCI/EAI 접근 환경 - -### 전체 빌드와 대표 검증 - -```powershell -.\gradlew.bat clean build -.\gradlew.bat :dap-tool-core:test -.\gradlew.bat :dap-tool-core:compileJava -``` - -### 애플리케이션 실행 - -각 애플리케이션은 별도 PowerShell에서 실행합니다. - -```powershell -# Gateway -.\gradlew.bat :dap-gateway:bootRun - -# SMS Tool -.\gradlew.bat :dap-tool-sms:bootRun - -# 기타 업무 Tool -.\gradlew.bat :dap-tool-oth:bootRun -``` - -기본 프로필은 `local`입니다. 개발 서버 설정이 필요하면 실행 환경에 프로필을 지정합니다. - -```powershell -$env:SPRING_PROFILES_ACTIVE = 'dev' -.\gradlew.bat :dap-tool-oth:bootRun -``` - -## Docker Compose 실행 - -Docker Compose는 Redis, Gateway, MCI Mock, SMS Tool, OTH Tool을 함께 기동합니다. - -```powershell -$env:ACTIVE_PROFILE = 'local' -docker compose up -d --build -docker compose ps -``` - -| 서비스 | 컨테이너 포트 | 호스트 포트 | -|---|---:|---:| -| Redis | 6379 | 6379 | -| Gateway | 8081 | 8281 | -| MCI Mock | 8080 | 8089 | -| SMS Tool | 8082 | 8282 | -| OTH Tool | 8084 | 8284 | - -컨테이너 내부와 PC 브라우저의 접속 주소는 다릅니다. - -```text -컨테이너 내부: http://gateway:8081, http://tool-oth:8084 -PC 브라우저: http://localhost:8281, http://localhost:8284 -``` - -## 환경 설정 - -### 프로필 - -기본 프로필은 `local`입니다. Tool 서버와 Gateway 모두 `local`, `dev` 프로필 파일을 사용합니다. - -| 프로필 | 목적 | 주요 설정 파일 | +| 대상 Pod | 로컬 URL | Docker Compose URL | |---|---|---| -| `local` | PC 개발·MCI Mock·H2 메모리 DB 기반 실행 | `application-local.yml` | -| `dev` | 개발 서버 배포 실행 | `application-dev.yml` | +| OTH | http://localhost:8084/tool-test-console.html | http://localhost:8284/tool-test-console.html | +| SMS | http://localhost:8082/tool-test-console.html | http://localhost:8282/tool-test-console.html | -프로필은 환경 변수로 지정합니다. +사용 방법은 다음과 같습니다. -```powershell -# 로컬 개발 -$env:SPRING_PROFILES_ACTIVE = 'local' +1. 대상 Tool Pod의 Console에 접속합니다. +2. Tool을 선택하고 `Schema 샘플 채우기`로 요청 JSON을 생성합니다. +3. 업무에 맞는 값으로 보정한 뒤 실행합니다. +4. 재사용할 요청은 `현재 요청 저장`으로 브라우저 `localStorage`에 저장합니다. -# 개발 서버 설정으로 실행 -$env:SPRING_PROFILES_ACTIVE = 'dev' +### Gateway Auto Tester + +`tester.html`은 Gateway에 등록된 Tool을 대상으로 다음 기능을 제공합니다. + +- Tool 목록 자동 조회 및 Tool별 실행 +- JSON Schema 기반 더미 요청 생성, 사용자 요청 JSON 저장 +- 필터된 Tool 일괄 실행 및 실패 Tool 재실행 +- 결과 차트, 실시간 실행 로그, CSV 내보내기, 호출 예제 복사 +- Tool별 자동 Ping(3초 간격) 및 Stress Test(현재 50건 동시 요청) + +> Auto Tester의 더미 요청 성공은 **통신·등록·기본 스키마 확인용 스모크 테스트**입니다. 실제 MCI/EAI 업무 규칙, 권한, 데이터 정합성은 보장하지 않습니다. Stress Test와 Auto Ping은 실제 Tool·레거시 호출을 유발할 수 있으므로 개발/테스트 환경 및 승인된 테스트 데이터에서만 사용합니다. + +UI에서 사용하는 Tailwind CSS와 Chart.js는 `dap-gateway/src/main/resources/static/lib`에 포함되어 있어, 화면 라이브러리 로딩을 위해 외부 CDN 연결이 필요하지 않습니다. + +## 6. 주요 HTTP API + +### Gateway API + +| 기능 | Method | 경로 | +|---|---|---| +| 등록 Tool 목록 | GET | `/mcp/api/v1/tools/list` | +| Gateway를 통한 Tool 호출 | POST | `/mcp/api/v1/tools/call` | +| Tool 문서 Markdown | GET | `/mcp/api/v1/tools/docs/markdown` | +| Tool 등록 | POST | `/mcp/api/v1/registry/register` | +| Tool 해제 | POST | `/mcp/api/v1/registry/deregister` | +| Heartbeat | POST | `/mcp/api/v1/registry/heartbeat` | + +호출 본문은 JSON-RPC 형식을 사용합니다. + +```json +{ + "jsonrpc": "2.0", + "method": "tools/call", + "params": { + "name": "oth.cmm.claim.search", + "arguments": { + "claimNo": "CLM2026070100120" + } + }, + "id": 1 +} ``` -### 환경별 연결 주소 비교 +### Tool Pod API -| 항목 | `local` 프로세스 실행 | Docker Compose | `dev` 프로필 | -|---|---|---|---| -| Gateway 접근 주소 | `http://localhost:8081` | 컨테이너 내부 `http://gateway:8081` | 배포 환경 Gateway URL | -| SMS Tool 주소 | `http://localhost:8082` | `http://tool-sms:8082` | `PORT` 기본 8082 | -| OTH Tool 주소 | `http://localhost:8084` | `http://tool-oth:8084` | `PORT` 기본 8084 | -| Redis 주소 | 로컬 Redis 또는 `localhost:6379` | `redis:6379` | 운영/개발 Redis 설정 | -| MCI/EAI 대상 | MCI Mock 또는 로컬 설정 | `mci-mock:8080` | 개발망 연동 설정 | -| 브라우저 Gateway 접속 | `http://localhost:8081` | `http://localhost:8281` | 운영·개발 도메인 | +| 기능 | Method | 경로 | +|---|---|---| +| 현재 Pod의 Tool Manifest | GET | `/tool-manifest` | +| 현재 Pod의 로컬 Tool 목록 | GET | `/mcp/api/v1/tools/local` | +| 현재 Pod에 직접 Tool 호출 | POST | `/mcp/{toolName}` | -`local`에서 프로세스를 직접 실행하면 Tool의 `AXHUB_GATEWAY_URL`은 `localhost`를 사용합니다. Docker에서는 각 컨테이너가 서로 다른 네트워크 공간에 있으므로 반드시 Compose 서비스 이름을 사용합니다. -### Gateway 환경 변수 +Agent나 외부 클라이언트의 표준 MCP 진입은 Gateway를 사용합니다. Pod 직접 호출은 개발·단위 테스트·Pod Console 용도로 사용합니다. -| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 | -|---|---|---|---| -| `SPRING_PROFILES_ACTIVE` | Gateway, 모든 Tool | 활성 Spring 프로필 | `local` | -| `OPENROUTER_API_KEY` | Gateway | Chat/LLM 호출 API 키 | 운영·개발 환경의 비밀 저장소에서 주입 | -| `SPRING_DATA_REDIS_HOST` | Gateway, 모든 Tool | Redis 호스트 | Docker: `redis` | -| `SPRING_DATA_REDIS_PORT` | Gateway, 모든 Tool | Redis 포트 | `6379` | -| `MCP_GATEWAY_FALLBACK_DEFAULT_URL` | Gateway | Registry에 없는 Tool의 기본 fallback URL | Docker: `http://tool-oth:8084` | -| `MCP_GATEWAY_FALLBACK_ROUTES_SMS` | Gateway | SMS 계열 fallback URL | Docker: `http://tool-sms:8082` | -| `ACTIVE_PROFILE` | Docker Compose | Compose가 `SPRING_PROFILES_ACTIVE`에 전달할 프로필 | `local` | +## 7. Tool 개발 규칙 -`OPENROUTER_API_KEY` 같은 인증값은 `application.yml`, README, Git 커밋에 직접 넣지 않습니다. 개발·운영 환경의 Secret, CI/CD 변수 또는 안전한 환경 변수로 주입합니다. +### 이름 -### Tool 서버 환경 변수 - -| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 | -|---|---|---|---| -| `PORT` | `dap-tool-sms`, `dap-tool-oth` | 개발 프로필에서 Tool 서버 포트 변경 | SMS `8082`, OTH `8084` | -| `AXHUB_GATEWAY_URL` | 모든 Tool | Tool 등록·Heartbeat 대상 Gateway 주소 | 로컬 `http://localhost:8081`, Docker `http://gateway:8081` | -| `AXHUB_TOOL_URL` | 모든 Tool | Gateway가 해당 Tool Pod를 호출할 주소 | 로컬 `http://localhost:{server.port}` | -| `GLOW_COMMUNICATION_MCI_HOMT` | Docker Tool 컨테이너 | MCI 대상 호스트 | 로컬 Compose는 `mci-mock` | -| `GLOW_COMMUNICATION_MCI_PORT` | Docker Tool 컨테이너 | MCI 대상 포트 | 로컬 Compose는 `8080` | -| `GLOW_COMMUNICATION_EAI_HOMT` | Docker Tool 컨테이너 | EAI 대상 호스트 | 로컬 Compose는 `mci-mock` | -| `GLOW_COMMUNICATION_EAI_PORT` | Docker Tool 컨테이너 | EAI 대상 포트 | 로컬 Compose는 `8080` | - -`AXHUB_TOOL_URL`은 반드시 Gateway가 실제로 접근 가능한 주소여야 합니다. PC에서 각각 실행할 때는 `localhost`를 사용하고, Docker 컨테이너 안에서는 `tool-sms`, `tool-oth` 같은 Compose 서비스 이름을 사용합니다. - -### 로컬 실행용 권장 설정 - -아래는 키 값 없이 로컬 프로세스를 실행하는 예시입니다. Redis를 Docker로 먼저 기동하거나 전체 Docker Compose를 사용합니다. - -```powershell -# 선택 1: Redis만 기동 -$env:ACTIVE_PROFILE = 'local' -docker compose up -d redis mci-mock - -# 선택 2: 각 프로세스를 로컬에서 기동 -$env:SPRING_PROFILES_ACTIVE = 'local' -$env:OPENROUTER_API_KEY = '<개인 또는 개발용 비밀 저장소의 키>' -.\gradlew.bat :dap-gateway:bootRun -``` - -다른 PowerShell에서 Tool을 실행합니다. - -```powershell -$env:SPRING_PROFILES_ACTIVE = 'local' -$env:AXHUB_GATEWAY_URL = 'http://localhost:8081' -$env:AXHUB_TOOL_URL = 'http://localhost:8084' -.\gradlew.bat :dap-tool-oth:bootRun -``` - -### 권한 도메인 설정 - -`mcp.security.tenant-domains`는 테넌트 또는 호출 주체가 접근할 수 있는 `categoryKey`를 정의합니다. 운영 환경에서는 `ALL`을 무분별하게 사용하지 않고, Agent·조직별 허용 도메인을 최소 권한으로 설정합니다. - -```yaml -mcp: - security: - tenant-domains: - claims-agent: claim, customer - notification-agent: notification -``` - -## 공통 기능과 운영 기준 - -### Redis - -Redis는 Tool Registry의 등록 상태와 Heartbeat, Gateway 실행 추적 정보에 사용됩니다. 현재 Redis는 PII 원문 보관소가 아닙니다. - -### 권한과 승인 - -Gateway는 Agent가 전달한 허용 Tool 목록, 서버 정책, 신뢰된 Claim 여부, 쓰기 승인 여부를 확인합니다. Tool마다 권한 로직을 중복 구현하지 말고 Gateway 공통 정책과 `@McpFunction` 메타데이터를 사용합니다. - -### Trace ID와 Request ID +Tool 함수명은 아래 4단계 규칙을 사용합니다. ```text -trace-id : 사용자 요청 전체에서 유지되는 상관관계 ID -request-id : Gateway → Tool, Tool → MCI 등 HTTP 호출마다 새로 생성되는 ID +pod.domain.service.action +예: oth.cmm.claim.search ``` -Gateway와 Tool에는 관련 헤더 및 MDC 기반 로그 처리가 있습니다. 신규 HTTP Client도 공통 전파 정책을 따르며, Tool별로 임의의 헤더 이름을 추가하지 않습니다. +- `pod`: Tool Pod 식별자 (`oth`, `sms` 등) +- `domain`: 업무 도메인 또는 `categoryKey` (`cmm`, `smp` 등) +- `service`: 업무 서비스명 +- `action`: 동작 (`search`, `issue`, `send` 등) -### 로그와 개인정보 - -Gateway에는 민감 키와 일부 형식을 마스킹하는 공통 기능이 있습니다. 마스킹은 원문을 Agent에서 분리하는 PII 토큰화와는 다릅니다. - -- 요청·응답 전문을 로그에 남길 때는 반드시 마스킹합니다. -- 운영 로그에 주민번호, 계좌번호, 전화번호, 이메일, 인증값을 남기지 않습니다. -- Tool 서버의 신규 로그도 같은 마스킹 정책을 적용합니다. -- 민감정보 복원 필요 여부는 Tool 개발자가 임의로 결정하지 않고 보안·AA 정책을 따릅니다. - -## AA 협의 기반 향후 전환 과제 - -아래 항목은 현재 구현 완료 기능이 아니라 회의에서 합의한 목표 구조입니다. - -### 부서별 Tool Pod와 저장소 경계 - -현재 `dap-tool-oth`에 함께 있는 업무 Tool을 부서·업무 소유권 단위로 분리합니다. 각 Pod는 독립 이미지, 독립 배포, 독립 장애 범위를 갖도록 구성합니다. 실제 분리는 AA가 확정한 Tool 소유 부서와 운영 책임 매핑을 기준으로 수행합니다. - -### Agent별 Tool 노출 수 제한 - -Agent에게 모든 Tool을 한 번에 제공하지 않고, 업무 도메인과 권한에 따라 약 10~20개 수준의 Tool 그룹을 제공합니다. `categoryKey`는 이를 위한 기초 메타데이터이며, 향후 Agent-Tool Group 정책으로 확장합니다. - -### PII 토큰화 +### 기본 구현 계층 ```text -원문 개인정보 - → PII Gateway가 Redis에 짧은 TTL로 보관 - → Agent에는 PII 토큰 또는 안전한 식별자만 전달 - → 인가된 Tool이 MCI 호출 직전에 필요한 항목만 복원 +Request DTO + -> UseCase Interface + -> UseCaseImpl + -> Converter (AI DTO <-> MCI/EAI Interface IO) + -> Client / Adapter + -> Legacy System ``` -이 전환 전까지는 현행 마스킹 기능을 PII 분리 구현으로 오해하지 않아야 합니다. +- Request DTO에는 `@McpValidation`으로 단순 입력 검증을 선언합니다. +- 복잡하거나 조건부 규칙이 필요한 입력은 `tool-schemas/{categoryKey}/...-input-schema.json` 리소스를 사용합니다. +- 응답은 `@McpOutputSchema`로 간단한 설명을 제공하고, 복잡한 응답은 Output Schema 리소스로 명시합니다. +- `null`의 업무 의미, 조건부 필드, 배열 정렬 기준, `hasMore` 여부, 민감 정보 제외 원칙을 Schema 설명에 명확히 씁니다. -### MCP SDK와 Glow Framework +### Schema 우선순위 -Gateway에는 Spring AI MCP Server 의존성이 포함되어 있습니다. MCP SDK/Glow 표준 적용 시에는 업무 Tool을 재작성하지 않고, 기존 `@McpTool`·`@McpFunction`과 UseCase를 표준 MCP Tool 명세·호출 콜백으로 연결하는 Adapter 계층을 공통 Core에 추가합니다. +1. `inputSchemaResource` 또는 `outputSchemaResource`가 지정된 경우: 해당 JSON Schema를 사용합니다. +2. 리소스가 없으면: DTO 어노테이션(`@McpValidation`, `@McpOutputSchema`)과 공통 Generator로 생성합니다. + +## 8. Manifest와 Registry + +각 Tool Pod는 기동 시 `@McpTool`, `@McpFunction` 정보를 읽어 `/tool-manifest`를 제공합니다. Manifest에는 Tool 이름, 설명, endpoint, revision, Input/Output Schema가 포함됩니다. + +Gateway는 Registry 등록 및 Heartbeat 정보를 이용해 Tool을 라우팅합니다. Tool Pod가 추가되어도 동일한 Manifest/Registry 규약을 준수하면 Gateway에서 탐색·호출할 수 있습니다. + +## 9. MCI/EAI 연계 + +공통 MCI/EAI 연계 기능과 Glow 기본 설정은 `dap-was-lib`에서 제공합니다. Tool Pod별 애플리케이션 프로필은 각 Pod의 `application*.yml`에서 관리합니다. ```text -MCP SDK 표준 tools/list, tools/call - ↓ -공통 Adapter - ↓ -기존 UseCase → Converter → MCI Client +dap-was-lib/src/main/resources/glow/application-glow*.yml +dap-was-sms/src/main/resources/application*.yml +dap-was-oth/src/main/resources/application*.yml ``` -따라서 업무 DTO, Converter, MCI Client의 책임은 유지됩니다. +MCI 호출 주소는 일반적으로 `host + uri`로 구성합니다. 예를 들어 `host=https://dev-ichmci.shinhanlife.co.kr`, `uri=/ntl_mci/clc_rcv`이면 호출 대상은 다음과 같습니다. -## 참고 소스 +```text +https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv +``` -| 주제 | 대표 위치 | +`receive-uri`는 Tool이 호출할 주소가 아니라, MCI가 비동기/콜백 방식으로 응답을 전달하도록 별도 계약된 경우 수신에 사용하는 경로입니다. +## 10. 품질 검증 + +```powershell +.\gradlew.bat :dap-was-lib:test +.\gradlew.bat :dap-was-oth:compileJava +.\gradlew.bat :dap-was-sms:compileJava +.\gradlew.bat validateMcpToolNames +``` + +배포 전에는 다음을 확인합니다. + +- Tool 이름의 전역 중복 여부와 `pod.domain.service.action` 규칙(아래 확인 사항 반영 후) +- Request/Response Schema 및 실제 예제 JSON +- Tool Pod 단위 테스트와 Gateway 경유 호출 +- MCI/EAI 오류 코드의 사용자용 응답 매핑 +- 민감정보가 요청·응답·로그에 포함되지 않는지 +- 승인된 개발/테스트 계정과 데이터만 사용했는지 + +## 11. 보안과 운영 원칙 + +- API Key, Runner 등록 토큰, 계정·비밀번호 등 비밀값은 Git에 넣지 않고 환경 변수 또는 Secret Manager로 주입합니다. +- `TESTER-DEV` 같은 테스트 권한은 개발 환경에서만 허용하고, 운영에서는 Tool 단위 최소 권한으로 제한합니다. +- Tool 요청·응답 로그는 마스킹/제외 정책을 거친 값만 남깁니다. +- Trace ID는 요청 흐름 전체를 추적하고, Request ID는 HTTP 호출 단위로 새로 부여합니다. +- Tool Pod는 업무 소유권과 장애 격리 단위에 맞춰 독립 이미지·배포·운영 책임으로 분리합니다. + +## 12. 참고 소스 위치 + +| 기능 | 위치 | |---|---| -| Gateway Tool API | `dap-gateway/.../presentation/McpRouterController.java` | -| 동적 MCP SSE/호출 경로 | `dap-gateway/.../sync/DynamicMcpController.java` | -| Tool 실행 Controller | `dap-tool-core/.../presentation/BusinessToolController.java` | -| Tool 자동 등록 | `dap-tool-core/.../usecase/ToolRegistryHeartbeatSender.java` | -| Tool 어노테이션 | `dap-tool-core/.../annotation/McpTool.java`, `McpFunction.java` | -| Tool 예시 | `dap-tool-oth/.../biz/oth`, `biz/sol`, `biz/smp` | -| SMS Tool 예시 | `dap-tool-sms/.../biz/sms` | -| Docker 환경 | `docker-compose.yml` | +| Tool 공통 Controller | `dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation` | +| Tool 어노테이션 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation` | +| JSON Schema/검증 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/schema`, `.../validation` | +| Manifest | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest` | +| Tool Scaffold | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java` | +| Gateway 라우터 | `dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java` | +| OTH Tool | `dap-was-oth/src/main/java` | +| SMS Tool | `dap-was-sms/src/main/java` | +## 13. 모듈명 전환 검증 ---- +Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니다. -문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다. -## Input/Output Schema 작성 가이드 - -Tool Schema는 Agent가 Tool을 정확히 호출하고, 반환값의 의미를 일관되게 해석하도록 하는 계약입니다. 인증 정보·사번·주민번호 등 민감정보(PII)는 Input/Output Schema와 Tool 응답에 포함하지 않습니다. - -### Input Schema - -Input Schema는 Agent가 Tool에 전달하는 파라미터의 이름, 타입, 필수 여부, 허용값, 형식 등을 정의합니다. - -적용 우선순위는 다음과 같습니다. - -1. `inputSchemaResource` — 복잡한 규칙을 담은 JSON Schema 리소스 -2. `inputSchema` — 어노테이션에 직접 선언한 JSON Schema -3. 요청 DTO 필드의 `@McpValidation` — 자동 JSON Schema 생성 - -단순한 요청 DTO는 `@McpValidation`만으로 관리합니다. - -```java -public class ClaimSearchRequest { - - @McpValidation(required = true, pattern = "^CLM[0-9]{13}$") - private String claimNo; - - @McpValidation(minimum = 1, maximum = 100) - private Integer size; -} -``` - -### Output Schema - -Output Schema는 Tool이 반환하는 결과의 타입과 의미를 정의합니다. `BusinessToolController`는 Tool 실행 후 반환값을 Output Schema 기준으로 검증합니다. - -적용 우선순위는 다음과 같습니다. - -1. `outputSchemaResource` — 조건부 필드·중첩 배열 등 복잡한 규칙을 담은 JSON Schema 리소스 -2. `outputSchema` — 어노테이션에 직접 선언한 JSON Schema -3. 반환 DTO의 `@McpOutputSchema`와 필드 `@McpValidation` — 자동 JSON Schema 생성 -4. 위 설정이 모두 없으면 Output Schema 검증을 수행하지 않음 - -따라서 단순한 응답은 별도 `outputSchemaResource` 없이 반환 DTO에 `@McpOutputSchema`를 선언하면 됩니다. `null`이 정상 값일 수 있는 필드는 `nullable = true`를 반드시 지정합니다. - -```java -@McpOutputSchema -public class ClaimSearchResponse { - - @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"}) - private String resultCode; - - @McpValidation(nullable = true, minimum = 0) - private Long approvedAmount; -} -``` - -### 복잡한 Schema는 Tool 모듈별 리소스로 관리 - -조건부 응답, 중첩 DTO, 배열 정렬 기준처럼 어노테이션만으로 표현하기 어려운 규칙은 Tool Core가 아니라 각 Tool 모듈의 리소스에 JSON Schema로 둡니다. - -```text -src/main/resources/ -└─ tool-schemas/ - └─ {categoryKey}/ - ├─ claim-search-resource-input-schema.json - └─ claim-search-resource-output-schema.json -``` - -예를 들어 `categoryKey`가 `cmm`이면 아래와 같이 선언합니다. - -```java -@McpFunction( - name = "oth.cmm.claim.search", - inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", - outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json" -) -public ClaimSearchResponse search(ClaimSearchRequest request) { - // ... -} -``` - -`inputSchemaResource`와 `outputSchemaResource`는 복잡한 경우에만 선언합니다. 단순한 Tool까지 JSON 파일을 별도 생성할 필요는 없습니다. - -### Output 설계 규칙 - -- 코드와 표시용 라벨을 함께 반환합니다. 예: `status` + `statusLabel` -- `null`이 정상인 값은 의미를 설명에 명시하고 DTO에는 `nullable = true`를 설정합니다. -- 조건부 필드는 어떤 조건에서 값이 존재하는지 JSON Schema에 명시합니다. -- 배열은 정렬 기준을 설명에 명시합니다. 예: `접수일 내림차순` -- 목록 응답에는 추가 조회 여부를 나타내는 `hasMore`를 포함합니다. -- 민감정보는 마스킹보다 **응답에서 제외**하는 것을 우선합니다. - -### 실행 로그 및 확인 - -Tool 실행이 끝나면 아래 로그는 Schema 정의가 아니라 **검증을 통과한 실제 최종 응답값**을 출력합니다. - -```text -[Tool -> MCP Gateway] Output Schema Result: { ... } -``` - -따라서 로그에도 실제 응답이 남으므로, 응답 DTO와 Output Schema에 민감정보가 포함되지 않도록 설계해야 합니다. - -스키마 리소스와 DTO 기반 자동 Schema는 아래 테스트로 함께 검증할 수 있습니다. - -```powershell -.\gradlew.bat :dap-tool-oth:test --tests "io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequestSchemaTest" -``` - -### Tool Naming Convention - -All Tool names use the four-level lowercase format `pod.domain.service.action`. Do not use underscores or CamelCase; use a hyphen (`-`) only when a single level has multiple words. - -- `pod`: deployment Tool Pod/module (`dap-tool-oth` → `oth`, `dap-tool-sms` → `sms`) -- `domain`: business-domain package (`cmm`, `smp`, `sol`, etc.) -- `service`: business service or resource -- `action`: the requested operation (`search`, `list`, `detail`, `issue`, `inquiry`, etc.) - -```text -oth.cmm.bond.issue -oth.cmm.claim.search -oth.sol.request.list -oth.smp.weather.inquiry -``` - -When Scaffold receives `dap-tool-oth`, `cmm`, and `ClaimSearch`, it generates `oth.cmm.claim.search`. The `validateMcpToolNames` Gradle task rejects both a duplicate name and any name outside this format before packaging, including its source file and line number. - -### Tool Test Console - -각 Tool Pod는 공통 테스트 화면을 제공합니다. - -```text -http://localhost:8084/tool-test-console.html -``` - -화면은 현재 Pod의 `/tool-manifest`에서 Tool 목록과 `inputSchema`를 읽습니다. Tool을 선택한 뒤 `Schema 샘플 채우기`로 요청 JSON을 만들고 실행할 수 있습니다. 업무에 맞게 보정한 요청은 `현재 요청 저장`으로 브라우저의 `localStorage`에 보관합니다. - -`Run saved cases`는 저장된 테스트 케이스를 순차 실행해 성공/실패, HTTP 상태, 소요 시간을 보여줍니다. 따라서 Tool이 수백 개여도 각 Tool마다 테스트 화면을 만들 필요 없이, 유효한 업무 테스트 데이터만 한 번 저장하면 이후에는 몇 번의 클릭으로 회귀 테스트할 수 있습니다. - -- Tool 호출은 현재 Pod의 `/mcp/{toolName}`로 수행합니다. -- 매 실행마다 `trace-id`, `request-id`를 새로 생성하여 응답과 함께 표시합니다. -- 외부 MCI/EAI Tool은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다. +- `validateMcpToolNames`는 `dap-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다. +- 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`를 검색합니다. \ No newline at end of file diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java index eff3c894..458ed107 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java @@ -35,9 +35,9 @@ public class ScaffoldingController { public String scaffoldPod(@RequestBody Map req) { try { String moduleName = req.getOrDefault("moduleName", "dap-was-oth"); - if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName; + if (!moduleName.startsWith("dap-was-")) moduleName = "dap-was-" + moduleName; String port = req.getOrDefault("port", "8085"); - String shortName = moduleName.replace("dap-tool-", "").replace("-", ""); + String shortName = moduleName.replace("dap-was-", "").replace("-", ""); String author = req.get("author"); if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name"); String date = req.get("date"); @@ -94,15 +94,15 @@ public class ScaffoldingController { if (sourceDir == null) sourceDir = System.getProperty("user.dir"); File dir = new File(sourceDir); - File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-was-lib")); + File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-was-") && !f.getName().equals("dap-was-lib")); if (files == null || files.length == 0) { - return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms"); + return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms"); } return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList()); } catch (Exception e) { - return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms"); + return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms"); } } } diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index 1c05d10e..cb61f1b1 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -488,9 +488,9 @@
Destination Pod directory.
@@ -665,7 +665,7 @@ .then(modules => { const select = document.getElementById('targetModuleSelect'); if (modules && modules.length > 0) { - select.innerHTML = modules.map(m => ``).join(''); + select.innerHTML = modules.map(m => ``).join(''); } }) .catch(err => console.error('Failed to load modules:', err)); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java index 49a613df..ea648ea7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java @@ -18,10 +18,10 @@ public class PodScaffolder { System.out.println(" MCP Tool Pod Scaffolder (Java CLI) "); System.out.println("=========================================\n"); - String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-tool-payment): "); - String moduleName = rawModuleName.startsWith("dap-tool-") ? rawModuleName : "dap-tool-" + rawModuleName; + String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-was-payment): "); + String moduleName = rawModuleName.startsWith("dap-was-") ? rawModuleName : "dap-was-" + rawModuleName; String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): "); - String shortName = moduleName.replace("dap-tool-", "").replace("-", ""); + String shortName = moduleName.replace("dap-was-", "").replace("-", ""); String defaultAuthor = System.getProperty("user.name"); String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java index 80357e4f..d2f3721a 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -673,8 +673,9 @@ public class ToolScaffolder { } private static String toToolName(String moduleName, String group, String baseName) { - String pod = moduleName.startsWith("dap-tool-") - ? moduleName.substring("dap-tool-".length()) + String moduleDirectory = Path.of(moduleName).getFileName().toString(); + String pod = moduleDirectory.startsWith("dap-was-") + ? moduleDirectory.substring("dap-was-".length()) : "oth"; String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2") .toLowerCase(Locale.ROOT) diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java index 506ca7d5..b1a0796f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java @@ -28,7 +28,7 @@ import java.util.stream.Stream; public class ToolSourceUpdater { public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception { - // 1. Find all *UseCase.java files in dap-tool-* directories + // 1. Find all *UseCase.java files in dap-was-* directories String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); @@ -37,7 +37,7 @@ public class ToolSourceUpdater { javaFiles = paths .filter(Files::isRegularFile) .filter(p -> p.toString().endsWith("UseCase.java")) - .filter(p -> p.toString().contains("dap-tool-") || p.toString().contains("axhub-tool-")) + .filter(p -> p.toString().contains("dap-was-") || p.toString().contains("axhub-tool-")) .collect(Collectors.toList()); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java index e26bfa3b..f491fd87 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidator.java @@ -38,7 +38,7 @@ public final class McpToolNameValidator { try (var modules = Files.list(projectRoot)) { modules.filter(Files::isDirectory) - .filter(path -> path.getFileName().toString().startsWith("dap-tool-")) + .filter(path -> path.getFileName().toString().startsWith("dap-was-")) .filter(path -> !path.getFileName().toString().equals("dap-was-lib")) .sorted() .forEach(module -> collectDeclarations(module, declarationsByName)); diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index f80f5011..09cbe51d 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -25,4 +25,17 @@ class ToolScaffolderTest { assertTrue(response.contains("@McpOutputSchema")); assertTrue(response.contains("@McpValidation")); } + @Test + void usesWasModuleNameAsToolPodPrefix() throws Exception { + String moduleName = "build/dap-was-sms"; + + ToolScaffolder.scaffold("notification send", "SMS0001", "SMS 발송", "cmm", "HTTP", moduleName, + "tester", "2026.08.05", true, null); + + Path useCasePath = Path.of(moduleName, + "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\"")); + } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java index 51048d6c..626817ef 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java @@ -31,21 +31,21 @@ class McpToolNameValidatorTest { @Test void rejectsDuplicateMcpFunctionNamesAcrossToolModules() throws IOException { - writeToolSource("dap-tool-first", "FirstTool.java", "first", "oth.sms.notification.send"); - writeToolSource("dap-tool-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("dap-tool-first")); - assertTrue(exception.getMessage().contains("dap-tool-second")); + assertTrue(exception.getMessage().contains("dap-was-first")); + assertTrue(exception.getMessage().contains("dap-was-second")); } @Test void validationRunnerRejectsDuplicateMcpFunctionNamesBeforePackaging() throws IOException { - writeToolSource("dap-tool-first", "FirstTool.java", "first", "oth.sms.notification.send"); - writeToolSource("dap-tool-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)); @@ -53,7 +53,7 @@ class McpToolNameValidatorTest { @Test void rejectsToolNameOutsidePodDomainServiceActionConvention() throws IOException { - writeToolSource("dap-tool-first", "FirstTool.java", "first", "bond_issue"); + writeToolSource("dap-was-first", "FirstTool.java", "first", "bond_issue"); IllegalStateException exception = assertThrows(IllegalStateException.class, () -> McpToolNameValidator.assertUnique(temporaryRoot));