From 1adbe5a2c7683ed413ffd5613b50bbdca9dab6ee Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 18 Aug 2026 12:50:09 +0900 Subject: [PATCH] =?UTF-8?q?read=20me=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 137 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ca7778d7e..5a6310d32 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ 신한라이프 업무 기능을 MCP(Model Context Protocol) Tool로 제공하는 Java 멀티 모듈 프로젝트입니다. 각 업무 모듈은 독립 실행 가능한 Spring Boot 애플리케이션이며, MCP Streamable HTTP와 REST 실행 API를 함께 제공합니다. -이 저장소에는 Gateway 애플리케이션이 포함되어 있지 않습니다. Tool Pod는 설정된 외부 Gateway에 Tool 등록을 시도하지만, Tool 조회와 직접 실행은 각 Pod가 자체적으로 처리합니다. +이 저장소에는 DAPMS(Gateway) 애플리케이션이 포함되어 있지 않습니다. DAPMT는 Gateway로 Tool을 push 등록하지 않으며, 각 Pod가 `GET /tool-manifest`를 제공하면 DAPMS가 이 Manifest를 pull하여 Tool 목록을 구성합니다. Tool 조회와 직접 실행은 각 Pod에서도 자체적으로 처리합니다. ## 기술 기준 @@ -42,8 +42,9 @@ Tool Pod │ └─ Spring Bean의 @McpTool 메서드 탐색 및 실행 메서드 캐시 ├─ ToolRegistryHeartbeatSender │ ├─ Tool 메타데이터 생성 - │ ├─ tool-definitions YAML 병합 - │ └─ 외부 Gateway 등록 시도 + │ └─ tool-definitions YAML 병합 + ├─ ToolManifestService + │ └─ DAPMS가 pull할 bundle 단위 Manifest 생성 ├─ McpToolExecutionService │ ├─ 입력 Schema 검증 │ ├─ 요청 DTO 변환 및 Tool 호출 @@ -59,6 +60,23 @@ Tool Pod 4. `ToolPodMcpToolSynchronizer`가 Tool을 MCP SDK 서버에 등록합니다. 5. REST와 MCP 요청은 공통 `McpToolExecutionService`를 통해 실행됩니다. +DAPMT 내부에는 `/registry/register`, `/registry/deregister` 호출이나 주기적인 Gateway heartbeat 전송이 없습니다. `ToolRegistryHeartbeatSender`라는 클래스명은 호환성을 위해 남아 있지만 현재 역할은 로컬 Tool 스캔과 메타데이터 생성뿐입니다. + +### DAPMS 연동 방식 + +```text +DAPMS + └─ GET {DAPMT Pod URL}/tool-manifest + └─ bundleId + revision + tools[] + └─ 각 Tool endpoint: {Pod URL}/mcp/{toolName} +``` + +- `mcp.manifest.bundle-id`는 Manifest를 제공하는 Pod의 고유 식별자이며 필수입니다. +- 현재 값은 `was-sal`, `was-cus`, `was-pro`, `was-sys`입니다. +- DAPMS에 설정한 bundle ID와 DAPMT가 반환하는 `bundleId`가 일치해야 같은 Tool bundle로 관리됩니다. +- `mcp.manifest.name-prefix`가 비어 있지 않으면 모든 Tool 이름이 해당 prefix로 시작해야 합니다. +- Manifest 내용이 바뀌면 `revision`이 증가하며, `If-None-Match`가 일치하면 `304 Not Modified`를 반환합니다. + ## 제공 API 각 업무 Pod가 동일한 API 구조를 제공합니다. @@ -79,8 +97,12 @@ Tool Pod ```powershell $headers = @{ - 'trace-id' = 'trace-local-001' - 'request-id' = 'request-local-001' + 'X-Tool-Server-API-Key' = $env:TOOL_SERVER_API_KEY + 'guid' = 'guid-local-001' + 'x-request-id' = 'request-local-001' + 'employee-no' = '100001' + 'virtual-employee-no' = 'V100001' + 'mcp-session-id' = 'session-local-001' } Invoke-RestMethod ` @@ -91,6 +113,21 @@ Invoke-RestMethod ` -Body '{"environment":"개발"}' ``` +### 요청 헤더 계약 + +DAPMS가 DAPMT Tool Service를 호출할 때 사용하는 헤더는 다음과 같습니다. HTTP 헤더 이름은 대소문자를 구분하지 않지만, 문서와 구현에서는 아래 표기를 기준으로 사용합니다. + +| 헤더 | 필수 여부 | 용도 | 전달 동작 | +|---|---|---|---| +| `X-Tool-Server-API-Key` | 인증 설정 시 필수 | DAPMS와 DAPMT 사이의 Tool Server 인증 | `mcp.security.api-key` 또는 `api-keys`와 비교 | +| `guid` | 선택 | 업무 호출 상관관계 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 | +| `x-request-id` | 선택 | 요청 추적 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 | +| `employee-no` | 선택 | 실제 사용자 사번 | 하위 HTTP 호출로 전달 | +| `virtual-employee-no` | 선택 | 가상 사용자 사번 | 하위 HTTP 호출로 전달 | +| `mcp-session-id` | 선택 | MCP 세션 식별자 | 성공 응답과 하위 HTTP 호출로 전달 | + +REST 경로 `/mcp/{toolName}`은 Controller가 위 헤더를 직접 읽습니다. MCP Streamable HTTP 경로 `/mcp`는 `McpRequestHeaderFilter`가 동일한 헤더를 `McpRequestHeaderContext`에 저장한 뒤 Tool 실행과 하위 HTTP 호출에 전달합니다. 헤더가 없는 하위 HTTP 호출에는 `X-ANONYMOUS-REQ: AXHUB-TOOL`이 설정됩니다. + 주요 실행 응답은 다음과 같습니다. | HTTP 상태 | 코드 | 의미 | @@ -150,6 +187,8 @@ V17 정의의 주요 필수 항목은 다음과 같습니다. 입력·출력 Schema는 `ToolSchemaResolver`가 어노테이션의 Schema 리소스와 인라인 Schema, DTO에서 생성한 Schema를 해석합니다. `ToolRegistryHeartbeatSender`는 여기에 YAML의 `parameters_schema`와 `output_schema`를 병합하여 최종 메타데이터를 만듭니다. +실행 시 Schema 검증은 MCP Java SDK의 `DefaultJsonSchemaValidator`를 사용하며 JSON Schema 2020-12 기준으로 처리합니다. 입력 불일치는 `422 INVALID_PARAM`, 출력 불일치는 `500 INVALID_TOOL_RESPONSE`로 반환됩니다. 단, Schema 해석 또는 검증기 자체에서 예외가 발생하면 현재 구현은 오류를 로그에 기록하고 해당 검증을 건너뜁니다. + ## 로컬 실행 ### 사전 조건 @@ -162,8 +201,11 @@ V17 정의의 주요 필수 항목은 다음과 같습니다. ```powershell $env:SPRING_PROFILES_ACTIVE = 'local' +$env:TOOL_SERVER_API_KEY = 'tool-server-key' ``` +`TOOL_SERVER_API_KEY`를 지정하지 않으면 현재 개발 기본값인 `tool-server-key`가 사용됩니다. 운영 환경에서는 기본값을 사용하지 말고 DAPMS의 Tool Server API Key와 동일한 별도 Secret을 주입해야 합니다. + 각 Pod는 별도 터미널에서 실행합니다. ```powershell @@ -198,13 +240,50 @@ 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` | +| `TOOL_SERVER_API_KEY` | DAPMS가 `X-Tool-Server-API-Key`로 전달할 공통 인증 Key | `tool-server-key` | +| `AXHUB_TOOL_URL` | Manifest의 Tool endpoint 생성에 사용할 Pod 외부 URL | `http://localhost:${server.port}` | +| `AXHUB_GATEWAY_URL` | 프로필 및 Compose 호환용 Gateway URL. 현재 DAPMT의 push 등록에는 사용하지 않음 | `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 대상 포트 | 프로필별 설정 | +`mcp.security.api-key`는 단일 DAPMS 공통 Key를, `mcp.security.api-keys`는 `API Key → tenant ID` 형태의 다중 Key를 지원합니다. 둘 중 하나라도 설정되어 있으면 올바른 `X-Tool-Server-API-Key`가 없는 `/rpc/**`, `/mcp/**` 요청은 `401 Unauthorized`가 됩니다. 두 설정이 모두 비어 있을 때만 익명 요청을 허용합니다. 현재 네 업무 Pod의 기본 `application.yml`은 단일 Key를 설정하므로 API Key 없이 Tool을 호출할 수 없습니다. + +## Scaffold + +공통 라이브러리는 새 Tool과 새 Tool Pod를 만드는 두 개의 Java CLI를 제공합니다. 두 클래스의 `main` 메서드를 IDE에서 실행하거나 필요한 인자를 전달해 실행할 수 있습니다. + +| Scaffolder | 역할 | 주요 생성·수정 대상 | +|---|---|---| +| `ToolScaffolder` | 기존 Pod에 Tool 구현 추가 | UseCase, DTO, Converter, Mock 응답, V17 Tool YAML, 연동 설정 | +| `PodScaffolder` | 새 실행 Pod 모듈 추가 | 모듈 디렉터리, `build.gradle`, Dockerfile, Application 클래스, 프로필 설정, `settings.gradle`, `docker-compose.yml` | + +`PodScaffolder`의 인자 순서는 다음과 같습니다. + +```text +PodScaffolder [author] [yyyy.MM.dd] +``` + +예를 들어 `payment 8099`를 입력하면 `dap-was-payment` 모듈을 생성합니다. 생성되는 `application.yml`에는 다음 계약이 포함됩니다. + +```yaml +mcp: + manifest: + bundle-id: dap-was-payment + name-prefix: "" + security: + api-key: ${TOOL_SERVER_API_KEY:tool-server-key} +``` + +생성되는 Compose 서비스에도 `TOOL_SERVER_API_KEY=${TOOL_SERVER_API_KEY:-tool-server-key}`가 추가됩니다. 생성 후에는 다음 항목을 반드시 확인해야 합니다. + +1. `bundle-id`를 DAPMS에 등록할 bundle ID와 일치시킵니다. +2. 운영 환경의 `TOOL_SERVER_API_KEY`를 DAPMS가 전달하는 Key와 동일한 Secret으로 설정합니다. +3. 실제 배포 주소에 맞게 `AXHUB_TOOL_URL`을 설정합니다. +4. MCI·EAI·HTTP 연동 대상과 timeout을 환경별 설정으로 교체합니다. +5. `validateMcpToolNames`, `validateToolSchemaV17`, 전체 테스트를 실행합니다. + ## 테스트와 검증 ```powershell @@ -235,19 +314,15 @@ http://localhost:8086/swagger-ui/index.html 2026-08-18 기준 확인 결과입니다. -- 전체 운영 소스 `classes`: 성공 +- 전체 `test`: 성공 +- 총 94개 테스트 성공, 실패·오류·건너뜀 0개 - `validateMcpToolNames`: 성공 - `validateToolSchemaV17`: 203개 Tool 성공 -- SAL·PRO·SYS 테스트: 20개 성공 -- 전체 `test`: 테스트 소스 컴파일 오류로 실패 - - LIB의 `ToolScaffolderTest`가 변경 전 `FieldDefinition` 생성자를 사용 - - LIB의 `ToolManifestServiceTest` 패키지와 테스트용 생성자 접근 범위가 불일치 - -전체 빌드의 기준을 회복하려면 위 테스트 소스 회귀를 먼저 정리해야 합니다. +- 실행 명령: `.\gradlew.bat test validateMcpToolNames validateToolSchemaV17` ## Docker Compose -Compose는 네 업무 Pod를 정의합니다. +`docker-compose.yml`은 DAPMT의 네 업무 Pod만 정의합니다. | 서비스 | 컨테이너 포트 | 호스트 포트 | |---|---:|---:| @@ -263,19 +338,36 @@ Compose는 네 업무 Pod를 정의합니다. docker compose up --build ``` -주의: `docker-compose.yml`은 `http://gateway:8081`을 Gateway 주소로 사용하지만 이 저장소의 Compose에는 `gateway` 서비스가 없습니다. 동일 Docker 네트워크에 Gateway를 제공하거나 `AXHUB_GATEWAY_URL`을 실제 접근 가능한 주소로 변경해야 합니다. +Compose 파일의 용도와 현재 주의점은 다음과 같습니다. + +| 파일 | 용도 | 현재 소스 기준 주의점 | +|---|---|---| +| `docker-compose.yml` | DAPMT 네 Pod 단독 실행 | `gateway` 서비스가 없지만 push 등록이 제거되어 DAPMT 시작에는 필요하지 않음 | +| `docker-compose.local.yml` | DAPMS와 DAPMT의 로컬 통합 구성 | 두 저장소가 같은 상위 디렉터리에 있는 구조를 가정 | +| `docker-compose.prod.yml` | DAPMS와 DAPMT의 개발 프로필 기반 OCI 구성 | 저장소의 runner 등록 토큰을 운영 Secret으로 분리해야 함 | + +`docker-compose.local.yml`과 `docker-compose.prod.yml`의 build context는 각각 `./dap-was-dapms`, `./dap-was-dapmt`입니다. 현재 파일 위치에서 사용할 때는 context 기준을 두 저장소의 상위 디렉터리로 맞춰야 합니다. + +```powershell +# DAPMT 저장소 디렉터리에서 실행 +docker compose --project-directory .. -f docker-compose.local.yml up --build +``` + +현재 기존 네 Pod의 Compose 정의에는 `TOOL_SERVER_API_KEY` 환경 변수 전달이 없습니다. 따라서 컨테이너는 애플리케이션 기본값 `tool-server-key`를 사용합니다. 운영 배포 전 각 서비스에 Secret 기반 `TOOL_SERVER_API_KEY` 전달 설정을 추가하고 DAPMS의 Key와 일치시켜야 합니다. DAPMS는 각 Pod의 `AXHUB_TOOL_URL` 또는 배포 URL에 접근해 `/tool-manifest`를 pull할 수 있어야 합니다. ## 보안 및 운영 주의사항 현재 구현을 운영 환경에 노출하기 전에 아래 항목을 반드시 점검해야 합니다. -- API Key 인터셉터는 `/rpc/**`, `/mcp/api/v1/**`에만 적용됩니다. 실제 Tool 실행 경로인 `/mcp/{toolName}`과 MCP 전송 경로 `/mcp`는 현재 검사 대상이 아닙니다. -- `mcp.security.api-keys`가 비어 있으면 인터셉터가 익명 요청을 허용합니다. 현재 기본 설정에는 API Key가 정의되어 있지 않습니다. +- API Key 인터셉터는 `/rpc/**`, `/mcp/**`에 적용됩니다. 따라서 `/mcp`, `/mcp/{toolName}`, `/mcp/api/v1/tools/local`은 인증 대상입니다. +- `/tool-manifest`는 위 인터셉터 경로 밖에 있어 현재 API Key 인증 대상이 아닙니다. 내부망·Ingress 정책 또는 별도 인증이 필요한지 운영 기준을 확인해야 합니다. +- 네 업무 Pod의 기본 Key는 모두 `tool-server-key`입니다. 운영에서는 반드시 별도 Secret으로 교체하고 DAPMS의 `X-Tool-Server-API-Key` 값과 일치시켜야 합니다. +- 설정된 단일 Key와 다중 Key가 모두 없을 때만 익명 요청이 허용됩니다. - `mcp.security.tenant-domains`는 설정 객체에 바인딩되지만 Tool별 인가에 사용되지 않습니다. - `requiresApproval`은 메타데이터에만 기록되며 실행 차단이나 승인 확인 로직은 없습니다. - 입력·출력 Schema 처리 자체에서 예외가 발생하면 현재 실행 서비스는 로그를 남기고 검증을 건너뜁니다. - CORS는 모든 Origin을 허용하면서 credential도 허용하도록 설정되어 있습니다. 운영 Origin을 명시적으로 제한해야 합니다. -- Gateway 등록은 별도 비관리 스레드에서 Tool별로 최대 12회 재시도합니다. Gateway 장애 시 장시간 실행될 수 있으므로 타임아웃과 종료 정책을 점검해야 합니다. +- `docker-compose.prod.yml`에 runner 등록 토큰이 평문으로 포함되어 있습니다. 사용 중인 토큰은 폐기·재발급하고 배포 Secret으로 이전해야 합니다. - Tool 요청과 연동 오류 로그에 개인정보나 인증정보가 포함되지 않도록 DTO와 로그 마스킹 정책을 검토해야 합니다. ## 주요 소스 위치 @@ -284,11 +376,13 @@ docker compose up --build |---|---| | 공통 빌드 및 검증 작업 | `build.gradle` | | REST Tool 실행 API | `dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java` | +| 요청 헤더 캡처·전달 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java`, `McpRequestHeaderContext.java` | | Tool 실행 서비스 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java` | +| JSON Schema 2020-12 검증 설정 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.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` | +| Tool 스캔 및 메타데이터 생성 | `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` | @@ -302,4 +396,5 @@ docker compose up --build 3. `validateMcpToolNames`와 `validateToolSchemaV17`을 실행합니다. 4. 모듈 테스트와 전체 테스트를 실행합니다. 5. 로컬 Pod에서 `/mcp/api/v1/tools/local`과 `/tool-manifest`를 확인합니다. -6. REST와 MCP 양쪽에서 동일한 Tool 결과와 오류 계약을 확인합니다. +6. DAPMS의 bundle ID, Pod Manifest URL, Tool Server API Key가 DAPMT 설정과 일치하는지 확인합니다. +7. REST와 MCP 양쪽에서 동일한 Tool 결과·요청 헤더 전달·오류 계약을 확인합니다.