From 867f2efabe2dcb32b518b18b576e95e047ac094b Mon Sep 17 00:00:00 2001 From: jade Date: Fri, 7 Aug 2026 09:04:04 +0900 Subject: [PATCH] Update: consolidate recent changes --- .agents/AGENTS.md | 39 - .agents/mcp.json | 7 - .agents/skills/chaos-tool-tester/SKILL.md | 16 - .../skills/gateway-routing-tracer/SKILL.md | 12 - .../skills/gateway-routing-tracer/trace.sh | 18 - .agents/skills/mcp-schema-autogen/SKILL.md | 16 - .agents/skills/mcp_best_practices/SKILL.md | 249 ------ .agents/skills/mcp_evaluation/SKILL.md | 602 -------------- .../dap/lib/adapter/sender/EaiEimsSender.java | 4 +- .../dap/lib/annotation/McpAnyOf.java | 35 - .../dap/lib/annotation/McpFunction.java | 76 -- .../dap/lib/annotation/McpOutputSchema.java | 33 - .../dap/lib/annotation/McpParameter.java | 26 - .../dap/lib/annotation/McpTool.java | 33 - .../dap/lib/annotation/McpValidation.java | 52 -- .../dap/lib/annotation/ToolHint.java | 32 + .../dap/lib/aop/ToolSlaMonitoringAspect.java | 8 +- .../lib/config/ToolSchemaConfiguration.java | 16 - .../dap/lib/dto/OperationType.java | 20 + .../lib/manifest/ToolManifestAnnotations.java | 16 - .../dap/lib/manifest/ToolManifestItem.java | 16 - .../dap/lib/manifest/ToolManifestMeta.java | 16 - .../lib/manifest/ToolManifestResponse.java | 16 - .../dap/lib/manifest/ToolManifestService.java | 22 +- .../dap/lib/mcp/LocalToolScanner.java | 142 ---- .../dap/lib/mcp/McpRequestHeaderContext.java | 16 - .../dap/lib/mcp/McpRequestHeaderFilter.java | 16 - .../dap/lib/mcp/McpRequestHeaders.java | 16 - .../lib/mcp/ToolMcpServerConfiguration.java | 30 +- .../lib/mcp/ToolPodMcpToolSynchronizer.java | 33 +- .../lib/mcp/ToolRegistryHeartbeatSender.java | 197 +++++ .../dap/lib/mcp/config/SwaggerConfig.java | 6 +- .../dap/lib/mcp/config/WebConfig.java | 2 +- .../mcp/exception/GlobalExceptionHandler.java | 8 +- .../presentation/BusinessToolController.java | 244 ------ .../presentation/ToolManifestController.java | 48 -- .../presentation/SsoRestController.java | 75 -- .../session/presentation/io/SsoResponse.java | 35 - .../dap/lib/util/JsonSchemaGenerator.java | 103 ++- .../dap/lib/util/PodScaffolder.java | 309 +++++++ .../dap/lib/util/ToolScaffolder.java | 783 ++++++++++++++++++ .../dap/lib/util/ToolSchemaResolver.java | 65 +- .../dap/lib/util/ToolSourceUpdater.java | 140 ++++ .../McpToolNameValidationRunner.java | 16 - .../lib/validation/McpToolNameValidator.java | 22 +- .../ToolArgumentSchemaValidator.java | 16 - .../shinhanlife/dap/mcc/dto/ToolMetadata.java | 79 +- .../presentation/BusinessToolController.java | 69 +- .../presentation/ToolManifestController.java | 18 +- .../annotation/GlowTrgmField.java | 16 - .../glow/communication/dto/CommonHeader.java | 16 - .../communication/dto/HeaderDefaults.java | 16 - .../glow/communication/dto/Transfer.java | 16 - .../exception/ItrfException.java | 16 - .../eai/component/GlowEaiComponent.java | 16 - .../mci/component/GlowMciComponent.java | 16 - .../util/CommonHeaderFactory.java | 16 - .../shinhanlife/glow/util/GlowMciParser.java | 16 - .../main/resources/glow/application-glow.yml | 2 +- .../resources/static/tool-test-console.html | 16 +- .../adapter/sender/ShinhanMciSenderTest.java | 2 +- .../config/ToolSchemaConfigurationTest.java | 16 - .../dap/lib/util/JsonSchemaGeneratorTest.java | 34 +- .../dap/lib/util/ToolScaffolderTest.java | 41 + .../dap/lib/util/ToolSchemaResolverTest.java | 61 +- .../dap/lib/util/ToolSourceUpdaterTest.java | 37 + .../validation/McpToolNameValidatorTest.java | 40 +- .../manifest/ToolManifestServiceTest.java | 19 +- .../mcp/McpRequestHeaderFilterTest.java | 35 +- .../mcp/ToolMcpServerConfigurationTest.java | 23 +- ...inessToolControllerHeaderContractTest.java | 18 +- .../ToolArgumentSchemaValidatorTest.java | 19 +- .../ToolTestConsoleResourceTest.java | 18 +- .../annotation/GlowTrgmFieldContractTest.java | 16 - .../dap/mcc/biz/cmm/dto/BalanceRequest.java | 14 +- .../biz/cmm/dto/BillingProcessRequest.java | 18 +- .../mcc/biz/cmm/dto/BillingStatusRequest.java | 10 +- .../dap/mcc/biz/cmm/dto/BondCheckRequest.java | 12 +- .../dap/mcc/biz/cmm/dto/BondIssueRequest.java | 20 +- .../mcc/biz/cmm/dto/ClaimSearchRequest.java | 53 +- .../mcc/biz/cmm/dto/ClaimSearchResponse.java | 71 +- .../biz/cmm/dto/ContractDetailRequest.java | 13 +- .../biz/cmm/dto/ContractStatusRequest.java | 12 +- .../biz/cmm/dto/CustomerDetailRequest.java | 12 +- .../mcc/biz/cmm/dto/CustomerGradeRequest.java | 12 +- .../mcc/biz/cmm/dto/LeaveCountRequest.java | 10 +- .../biz/cmm/dto/MciSampleStringResponse.java | 16 - .../mcc/biz/cmm/dto/MciSampleTargetDto.java | 16 - .../biz/cmm/dto/MetaCommonCodeRequest.java | 17 +- .../dap/mcc/biz/cmm/dto/MetaTableRequest.java | 18 +- .../mcc/biz/cmm/dto/SampleStringRequest.java | 5 +- .../biz/cmm/dto/TemplateDownloadRequest.java | 3 + .../biz/cmm/dto/VacationRegisterRequest.java | 18 +- .../mcc/biz/cmm/usecase/BalanceUseCase.java | 27 +- .../cmm/usecase/BillingProcessUseCase.java | 27 +- .../mcc/biz/cmm/usecase/BondIssueUseCase.java | 26 +- .../ClaimSearchSchemaSampleUseCase.java | 40 +- .../biz/cmm/usecase/CommonUtilityUseCase.java | 29 +- .../cmm/usecase/ContractInquiryUseCase.java | 26 +- .../biz/cmm/usecase/CustomerInfoUseCase.java | 26 +- .../cmm/usecase/MetaCommonCodeUseCase.java | 21 +- .../mcc/biz/cmm/usecase/MetaTableUseCase.java | 21 +- .../cmm/usecase/TemplateUtilityUseCase.java | 31 +- .../ClaimSearchSchemaSampleUseCaseImpl.java | 16 - .../biz/oth/converter/Onnba3011Converter.java | 16 - .../dap/mcc/biz/oth/dto/Onnba3011Request.java | 16 - .../mcc/biz/oth/usecase/Onnba3011UseCase.java | 23 +- .../usecase/impl/Onnba3011UseCaseImpl.java | 2 - .../mcc/biz/smp/dto/DailyQuoteRequest.java | 30 +- .../mcc/biz/smp/dto/DailyQuoteResponse.java | 16 - .../mcc/biz/smp/dto/ExchangeRateRequest.java | 29 +- .../mcc/biz/smp/dto/ExchangeRateResponse.java | 16 - .../mcc/biz/smp/dto/TeamMemberRequest.java | 12 +- .../dap/mcc/biz/smp/dto/WeatherRequest.java | 8 +- .../smp/usecase/DailyQuoteToolUseCase.java | 30 +- .../smp/usecase/ExchangeRateToolUseCase.java | 30 +- .../biz/smp/usecase/TeamMemberUseCase.java | 35 +- .../biz/smp/usecase/WeatherToolUseCase.java | 30 +- .../impl/ExchangeRateToolUseCaseImpl.java | 2 +- .../mcc/biz/sol/dto/SolReqDetailRequest.java | 12 +- .../mcc/biz/sol/dto/SolReqListRequest.java | 17 +- .../biz/sol/usecase/SolReqDetailUseCase.java | 22 +- .../biz/sol/usecase/SolReqListUseCase.java | 35 +- .../usecase/impl/SolReqDetailUseCaseImpl.java | 2 +- .../usecase/impl/SolReqListUseCaseImpl.java | 2 +- .../itrf/mci/cfp/a/io/CLCNNB00001_I.java | 16 - .../itrf/mci/cfp/a/io/CLCNNB00001_O.java | 16 - .../dap/mcc/oth/DapWasOthApplication.java | 3 + .../src/main/resources/application-local.yml | 4 +- .../src/main/resources/application.yml | 2 +- .../claim-search-resource-input-schema.json | 49 ++ .../claim-search-resource-output-schema.json | 83 ++ .../cmm/dto/ClaimSearchRequestSchemaTest.java | 33 +- .../impl/Onnba3011UseCaseImplTest.java | 16 - .../impl/SolReqListUseCaseImplTest.java | 16 - .../Onnba3011MciRequestConverterTest.java | 16 - .../dap/mcc/biz/sms/dto/SmsSendRequest.java | 18 +- .../mcc/biz/sms/usecase/SmsToolUseCase.java | 23 +- .../src/main/resources/application-local.yml | 4 +- .../plans/2026-08-04-readme-current-was.md | 62 -- .../2026-08-06-mcp-sdk-validator-updater.md | 61 ++ 141 files changed, 2392 insertions(+), 3464 deletions(-) delete mode 100644 .agents/AGENTS.md delete mode 100644 .agents/mcp.json delete mode 100644 .agents/skills/chaos-tool-tester/SKILL.md delete mode 100644 .agents/skills/gateway-routing-tracer/SKILL.md delete mode 100644 .agents/skills/gateway-routing-tracer/trace.sh delete mode 100644 .agents/skills/mcp-schema-autogen/SKILL.md delete mode 100644 .agents/skills/mcp_best_practices/SKILL.md delete mode 100644 .agents/skills/mcp_evaluation/SKILL.md delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/BusinessToolController.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/ToolManifestController.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java delete mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java create mode 100644 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java create mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java rename dap-was-lib/src/test/java/io/shinhanlife/dap/{lib => mcc}/manifest/ToolManifestServiceTest.java (92%) rename dap-was-lib/src/test/java/io/shinhanlife/dap/{lib => mcc}/mcp/McpRequestHeaderFilterTest.java (70%) rename dap-was-lib/src/test/java/io/shinhanlife/dap/{lib => mcc}/mcp/ToolMcpServerConfigurationTest.java (58%) rename dap-was-lib/src/test/java/io/shinhanlife/dap/{lib/validation => mcc/presentation}/ToolArgumentSchemaValidatorTest.java (66%) create mode 100644 dap-was-oth/src/main/resources/mcp/schema/claim-search-resource-input-schema.json create mode 100644 dap-was-oth/src/main/resources/mcp/schema/claim-search-resource-output-schema.json delete mode 100644 docs/superpowers/plans/2026-08-04-readme-current-was.md create mode 100644 docs/superpowers/plans/2026-08-06-mcp-sdk-validator-updater.md diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md deleted file mode 100644 index 37f6daf02..000000000 --- a/.agents/AGENTS.md +++ /dev/null @@ -1,39 +0,0 @@ -# AX HUB 프로젝트 개발 규칙 (AGENTS.md) - -이 파일은 AI Agent(Antigravity)가 이 프로젝트에서 작업할 때 항상 명심하고 지켜야 할 규칙을 정의하는 파일입니다. -작업 시 유의해야 할 사항이 생기면 언제든지 이 아래에 자유롭게 내용을 추가해 주세요! - -## 개발 가이드라인 -* 패키지명은 `controller` 대신 `presentation`을 사용합니다. -* MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트) - -## 명심해야 할 규칙 추가란 -* `application.yml` 등 설정 파일 수정 시 한글이 깨지지 않도록 항상 UTF-8 인코딩을 유지하고, 깨진 문자열(`?\uFFFD` 등)이 발생하지 않도록 각별히 주의한다. -* 이모지는 무조건 넣지 않는다 -* import 할것 무조건 한다 -* Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다. -* 자바 만들때는 무조건 아래 내용을 넣는다 -/** - * @package io.shinhanlife.axhub.biz.mcp.tool.sms - * @className AxHubToolSmsApplication - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -* 신한라이프 인터페이스 통신 프로토콜 및 메시지 포맷 가이드 (준버티컬 Tool 대상): - * 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외) - * 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행) - * 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다. - -* 신한라이프 표준 로그 기준 (Logback 설정): - * **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`) - * **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`) - * **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다. - * **호스트명 동적 할당:** Logback 설정 시 `` 를 선언하여 사용한다. diff --git a/.agents/mcp.json b/.agents/mcp.json deleted file mode 100644 index 891203db6..000000000 --- a/.agents/mcp.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "mcpServers": { - "axhub-gateway": { - "url": "http://localhost:8281/mcp/sse" - } - } -} diff --git a/.agents/skills/chaos-tool-tester/SKILL.md b/.agents/skills/chaos-tool-tester/SKILL.md deleted file mode 100644 index 154d167f5..000000000 --- a/.agents/skills/chaos-tool-tester/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: chaos-tool-tester -description: 현재 연결된 MCP 도구들에 대해 엣지 케이스(Null, 타입 오류, SQL 인젝션 문자열 등)를 고의로 주입하여 안정성을 테스트합니다. -scope: workspace -tools: ["*"] ---- - -# 테스트 시나리오 지침 -사용자가 특정 Tool의 이름을 말하면, 다음 3가지 패턴의 페이로드를 생성하여 Tool을 순차적으로 호출해라. - -1. **타입 브레이커:** Integer가 들어가야 할 곳에 긴 String 넣기 -2. **누락 테스트:** 필수(Required) 파라미터를 누락시키고 호출하기 -3. **악의적 페이로드:** 파라미터 값에 `` 또는 `' OR 1=1 --` 삽입하기 - -# 결과 리포팅 -각 호출 시도 후, Spring Boot 서버가 500 에러를 뱉고 죽었는지, 아니면 안전하게 Validation 에러 메시지를 반환했는지 마크다운 표로 정리해서 보고할 것. diff --git a/.agents/skills/gateway-routing-tracer/SKILL.md b/.agents/skills/gateway-routing-tracer/SKILL.md deleted file mode 100644 index f5eb891c6..000000000 --- a/.agents/skills/gateway-routing-tracer/SKILL.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: gateway-routing-tracer -description: 게이트웨이 로그를 분석하여 특정 Tool Call이나 Request ID의 라우팅 경로와 소요 시간을 추적합니다. -scope: workspace ---- - -# 실행 스크립트 -이 스킬은 같은 폴더에 위치한 `trace.sh` 스크립트를 실행하여 로그를 파싱합니다. - -# 에이전트 지침 -1. 사용자가 조회하고자 하는 `Request ID` 또는 `Tool 이름`을 감지하면, 백그라운드 터미널에서 `trace.sh <인자>`를 실행해라. -2. 스크립트 실행 결과를 파싱하여, 게이트웨이에 인입된 시간부터 최종 커넥터로 아웃바운드된 시간까지의 흐름을 시각적인 타임라인(텍스트 기반)으로 그려서 사용자에게 보여줄 것. diff --git a/.agents/skills/gateway-routing-tracer/trace.sh b/.agents/skills/gateway-routing-tracer/trace.sh deleted file mode 100644 index 41af8631c..000000000 --- a/.agents/skills/gateway-routing-tracer/trace.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# Spring Boot 로그 파일 경로 (기본 설정: ./logs/spring-boot-logger.log) -LOG_FILE="./logs/spring-boot-logger.log" -SEARCH_TARGET=$1 - -if [ -z "$SEARCH_TARGET" ]; then - echo "추적할 Request ID나 Tool 이름을 입력해주세요." - exit 1 -fi - -if [ ! -f "$LOG_FILE" ]; then - echo "오류: 로그 파일($LOG_FILE)을 찾을 수 없습니다. 경로를 확인해주세요." - exit 1 -fi - -echo "[$SEARCH_TARGET] 라우팅 추적 시작..." -# Spring Boot 로그에서 대상 검색하여 주요 필드 출력 -grep -i "$SEARCH_TARGET" "$LOG_FILE" | awk '{print $1, $2, $3, $5, $NF}' diff --git a/.agents/skills/mcp-schema-autogen/SKILL.md b/.agents/skills/mcp-schema-autogen/SKILL.md deleted file mode 100644 index f83f77f96..000000000 --- a/.agents/skills/mcp-schema-autogen/SKILL.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: mcp-schema-autogen -description: Java 21 Record 클래스나 Spring Controller 코드를 분석하여 MCP 표준 JSON Schema(Tool 명세)를 자동 생성합니다. -scope: workspace ---- - -# 시스템 지침 (System Prompt) -너는 MCP(Model Context Protocol) Tool 스키마 전문 엔지니어다. -사용자가 Java 코드를 제공하면 다음 규칙에 따라 JSON Schema를 작성해라. - -1. **타입 매핑:** Java 21 `Record`의 필드 타입을 완벽하게 JSON Schema 타입(string, integer, boolean, object, array 등)으로 매핑할 것. -2. **필수값 추출:** `@NotNull`, `@NotBlank` 또는 Java 21 Record의 Non-null 필드는 반드시 `required` 배열에 포함시킬 것. -3. **설명 추가:** JavaDoc 주석이 있거나 필드 위에 주석이 있다면 스키마의 `description` 필드로 적극 반영할 것. - -# 출력 형식 -결과물은 반드시 순수한 JSON 형태로만 출력하고, 불필요한 부연 설명은 생략한다. diff --git a/.agents/skills/mcp_best_practices/SKILL.md b/.agents/skills/mcp_best_practices/SKILL.md deleted file mode 100644 index b8e949cf4..000000000 --- a/.agents/skills/mcp_best_practices/SKILL.md +++ /dev/null @@ -1,249 +0,0 @@ -# MCP Server Best Practices - -## Quick Reference - -### Server Naming -- **Python**: `{service}_mcp` (e.g., `slack_mcp`) -- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`) - -### Tool Naming -- Use snake_case with service prefix -- Format: `{service}_{action}_{resource}` -- Example: `slack_send_message`, `github_create_issue` - -### Response Formats -- Support both JSON and Markdown formats -- JSON for programmatic processing -- Markdown for human readability - -### Pagination -- Always respect `limit` parameter -- Return `has_more`, `next_offset`, `total_count` -- Default to 20-50 items - -### Transport -- **Streamable HTTP**: For remote servers, multi-client scenarios -- **stdio**: For local integrations, command-line tools -- Avoid SSE (deprecated in favor of streamable HTTP) - ---- - -## Server Naming Conventions - -Follow these standardized naming patterns: - -**Python**: Use format `{service}_mcp` (lowercase with underscores) -- Examples: `slack_mcp`, `github_mcp`, `jira_mcp` - -**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens) -- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server` - -The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers. - ---- - -## Tool Naming and Design - -### Tool Naming - -1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info` -2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers - - Use `slack_send_message` instead of just `send_message` - - Use `github_create_issue` instead of just `create_issue` -3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.) -4. **Be specific**: Avoid generic names that could conflict with other servers - -### Tool Design - -- Tool descriptions must narrowly and unambiguously describe functionality -- Descriptions must precisely match actual functionality -- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) -- Keep tool operations focused and atomic - ---- - -## Response Formats - -All tools that return data should support multiple formats: - -### JSON Format (`response_format="json"`) -- Machine-readable structured data -- Include all available fields and metadata -- Consistent field names and types -- Use for programmatic processing - -### Markdown Format (`response_format="markdown"`, typically default) -- Human-readable formatted text -- Use headers, lists, and formatting for clarity -- Convert timestamps to human-readable format -- Show display names with IDs in parentheses -- Omit verbose metadata - ---- - -## Pagination - -For tools that list resources: - -- **Always respect the `limit` parameter** -- **Implement pagination**: Use `offset` or cursor-based pagination -- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count` -- **Never load all results into memory**: Especially important for large datasets -- **Default to reasonable limits**: 20-50 items is typical - -Example pagination response: -```json -{ - "total": 150, - "count": 20, - "offset": 0, - "items": [...], - "has_more": true, - "next_offset": 20 -} -``` - ---- - -## Transport Options - -### Streamable HTTP - -**Best for**: Remote servers, web services, multi-client scenarios - -**Characteristics**: -- Bidirectional communication over HTTP -- Supports multiple simultaneous clients -- Can be deployed as a web service -- Enables server-to-client notifications - -**Use when**: -- Serving multiple clients simultaneously -- Deploying as a cloud service -- Integration with web applications - -### stdio - -**Best for**: Local integrations, command-line tools - -**Characteristics**: -- Standard input/output stream communication -- Simple setup, no network configuration needed -- Runs as a subprocess of the client - -**Use when**: -- Building tools for local development environments -- Integrating with desktop applications -- Single-user, single-session scenarios - -**Note**: stdio servers should NOT log to stdout (use stderr for logging) - -### Transport Selection - -| Criterion | stdio | Streamable HTTP | -|-----------|-------|-----------------| -| **Deployment** | Local | Remote | -| **Clients** | Single | Multiple | -| **Complexity** | Low | Medium | -| **Real-time** | No | Yes | - ---- - -## Security Best Practices - -### Authentication and Authorization - -**OAuth 2.1**: -- Use secure OAuth 2.1 with certificates from recognized authorities -- Validate access tokens before processing requests -- Only accept tokens specifically intended for your server - -**API Keys**: -- Store API keys in environment variables, never in code -- Validate keys on server startup -- Provide clear error messages when authentication fails - -### Input Validation - -- Sanitize file paths to prevent directory traversal -- Validate URLs and external identifiers -- Check parameter sizes and ranges -- Prevent command injection in system calls -- Use schema validation (Pydantic/Zod) for all inputs - -### Error Handling - -- Don't expose internal errors to clients -- Log security-relevant errors server-side -- Provide helpful but not revealing error messages -- Clean up resources after errors - -### DNS Rebinding Protection - -For streamable HTTP servers running locally: -- Enable DNS rebinding protection -- Validate the `Origin` header on all incoming connections -- Bind to `127.0.0.1` rather than `0.0.0.0` - ---- - -## Tool Annotations - -Provide annotations to help clients understand tool behavior: - -| Annotation | Type | Default | Description | -|-----------|------|---------|-------------| -| `readOnlyHint` | boolean | false | Tool does not modify its environment | -| `destructiveHint` | boolean | true | Tool may perform destructive updates | -| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect | -| `openWorldHint` | boolean | true | Tool interacts with external entities | - -**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations. - ---- - -## Error Handling - -- Use standard JSON-RPC error codes -- Report tool errors within result objects (not protocol-level errors) -- Provide helpful, specific error messages with suggested next steps -- Don't expose internal implementation details -- Clean up resources properly on errors - -Example error handling: -```typescript -try { - const result = performOperation(); - return { content: [{ type: "text", text: result }] }; -} catch (error) { - return { - isError: true, - content: [{ - type: "text", - text: `Error: ${error.message}. Try using filter='active_only' to reduce results.` - }] - }; -} -``` - ---- - -## Testing Requirements - -Comprehensive testing should cover: - -- **Functional testing**: Verify correct execution with valid/invalid inputs -- **Integration testing**: Test interaction with external systems -- **Security testing**: Validate auth, input sanitization, rate limiting -- **Performance testing**: Check behavior under load, timeouts -- **Error handling**: Ensure proper error reporting and cleanup - ---- - -## Documentation Requirements - -- Provide clear documentation of all tools and capabilities -- Include working examples (at least 3 per major feature) -- Document security considerations -- Specify required permissions and access levels -- Document rate limits and performance characteristics diff --git a/.agents/skills/mcp_evaluation/SKILL.md b/.agents/skills/mcp_evaluation/SKILL.md deleted file mode 100644 index e2c502ecc..000000000 --- a/.agents/skills/mcp_evaluation/SKILL.md +++ /dev/null @@ -1,602 +0,0 @@ -# MCP Server Evaluation Guide - -## Overview - -This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided. - ---- - -## Quick Reference - -### Evaluation Requirements -- Create 10 human-readable questions -- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE -- Each question requires multiple tool calls (potentially dozens) -- Answers must be single, verifiable values -- Answers must be STABLE (won't change over time) - -### Output Format -```xml - - - Your question here - Single verifiable answer - - -``` - ---- - -## Purpose of Evaluations - -The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions. - -## Evaluation Overview - -Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be: -- Realistic -- Clear and concise -- Unambiguous -- Complex, requiring potentially dozens of tool calls or steps -- Answerable with a single, verifiable value that you identify in advance - -## Question Guidelines - -### Core Requirements - -1. **Questions MUST be independent** - - Each question should NOT depend on the answer to any other question - - Should not assume prior write operations from processing another question - -2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use** - - Should not instruct or require modifying state to arrive at the correct answer - -3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX** - - Must require another LLM to use multiple (potentially dozens of) tools or steps to answer - -### Complexity and Depth - -4. **Questions must require deep exploration** - - Consider multi-hop questions requiring multiple sub-questions and sequential tool calls - - Each step should benefit from information found in previous questions - -5. **Questions may require extensive paging** - - May need paging through multiple pages of results - - May require querying old data (1-2 years out-of-date) to find niche information - - The questions must be DIFFICULT - -6. **Questions must require deep understanding** - - Rather than surface-level knowledge - - May pose complex ideas as True/False questions requiring evidence - - May use multiple-choice format where LLM must search different hypotheses - -7. **Questions must not be solvable with straightforward keyword search** - - Do not include specific keywords from the target content - - Use synonyms, related concepts, or paraphrases - - Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer - -### Tool Testing - -8. **Questions should stress-test tool return values** - - May elicit tools returning large JSON objects or lists, overwhelming the LLM - - Should require understanding multiple modalities of data: - - IDs and names - - Timestamps and datetimes (months, days, years, seconds) - - File IDs, names, extensions, and mimetypes - - URLs, GIDs, etc. - - Should probe the tool's ability to return all useful forms of data - -9. **Questions should MOSTLY reflect real human use cases** - - The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about - -10. **Questions may require dozens of tool calls** - - This challenges LLMs with limited context - - Encourages MCP server tools to reduce information returned - -11. **Include ambiguous questions** - - May be ambiguous OR require difficult decisions on which tools to call - - Force the LLM to potentially make mistakes or misinterpret - - Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER - -### Stability - -12. **Questions must be designed so the answer DOES NOT CHANGE** - - Do not ask questions that rely on "current state" which is dynamic - - For example, do not count: - - Number of reactions to a post - - Number of replies to a thread - - Number of members in a channel - -13. **DO NOT let the MCP server RESTRICT the kinds of questions you create** - - Create challenging and complex questions - - Some may not be solvable with the available MCP server tools - - Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN) - - Questions may require dozens of tool calls to complete - -## Answer Guidelines - -### Verification - -1. **Answers must be VERIFIABLE via direct string comparison** - - If the answer can be re-written in many formats, clearly specify the output format in the QUESTION - - Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else." - - Answer should be a single VERIFIABLE value such as: - - User ID, user name, display name, first name, last name - - Channel ID, channel name - - Message ID, string - - URL, title - - Numerical quantity - - Timestamp, datetime - - Boolean (for True/False questions) - - Email address, phone number - - File ID, file name, file extension - - Multiple choice answer - - Answers must not require special formatting or complex, structured output - - Answer will be verified using DIRECT STRING COMPARISON - -### Readability - -2. **Answers should generally prefer HUMAN-READABLE formats** - - Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d - - Rather than opaque IDs (though IDs are acceptable) - - The VAST MAJORITY of answers should be human-readable - -### Stability - -3. **Answers must be STABLE/STATIONARY** - - Look at old content (e.g., conversations that have ended, projects that have launched, questions answered) - - Create QUESTIONS based on "closed" concepts that will always return the same answer - - Questions may ask to consider a fixed time window to insulate from non-stationary answers - - Rely on context UNLIKELY to change - - Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later - -4. **Answers must be CLEAR and UNAMBIGUOUS** - - Questions must be designed so there is a single, clear answer - - Answer can be derived from using the MCP server tools - -### Diversity - -5. **Answers must be DIVERSE** - - Answer should be a single VERIFIABLE value in diverse modalities and formats - - User concept: user ID, user name, display name, first name, last name, email address, phone number - - Channel concept: channel ID, channel name, channel topic - - Message concept: message ID, message string, timestamp, month, day, year - -6. **Answers must NOT be complex structures** - - Not a list of values - - Not a complex object - - Not a list of IDs or strings - - Not natural language text - - UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON - - And can be realistically reproduced - - It should be unlikely that an LLM would return the same list in any other order or format - -## Evaluation Process - -### Step 1: Documentation Inspection - -Read the documentation of the target API to understand: -- Available endpoints and functionality -- If ambiguity exists, fetch additional information from the web -- Parallelize this step AS MUCH AS POSSIBLE -- Ensure each subagent is ONLY examining documentation from the file system or on the web - -### Step 2: Tool Inspection - -List the tools available in the MCP server: -- Inspect the MCP server directly -- Understand input/output schemas, docstrings, and descriptions -- WITHOUT calling the tools themselves at this stage - -### Step 3: Developing Understanding - -Repeat steps 1 & 2 until you have a good understanding: -- Iterate multiple times -- Think about the kinds of tasks you want to create -- Refine your understanding -- At NO stage should you READ the code of the MCP server implementation itself -- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks - -### Step 4: Read-Only Content Inspection - -After understanding the API and tools, USE the MCP server tools: -- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY -- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions -- Should NOT call any tools that modify state -- Will NOT read the code of the MCP server implementation itself -- Parallelize this step with individual sub-agents pursuing independent explorations -- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations -- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT -- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration -- In all tool call requests, use the `limit` parameter to limit results (<10) -- Use pagination - -### Step 5: Task Generation - -After inspecting the content, create 10 human-readable questions: -- An LLM should be able to answer these with the MCP server -- Follow all question and answer guidelines above - -## Output Format - -Each QA pair consists of a question and an answer. The output should be an XML file with this structure: - -```xml - - - Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? - Website Redesign - - - Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. - sarah_dev - - - Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs? - 7 - - - Find the repository with the most stars that was created before 2023. What is the repository name? - data-pipeline - - -``` - -## Evaluation Examples - -### Good Questions - -**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)** -```xml - - Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository? - Python - -``` - -This question is good because: -- Requires multiple searches to find archived repositories -- Needs to identify which had the most forks before archival -- Requires examining repository details for the language -- Answer is a simple, verifiable value -- Based on historical (closed) data that won't change - -**Example 2: Requires understanding context without keyword matching (Project Management MCP)** -```xml - - Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time? - Product Manager - -``` - -This question is good because: -- Doesn't use specific project name ("initiative focused on improving customer onboarding") -- Requires finding completed projects from specific timeframe -- Needs to identify the project lead and their role -- Requires understanding context from retrospective documents -- Answer is human-readable and stable -- Based on completed work (won't change) - -**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)** -```xml - - Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username. - alex_eng - -``` - -This question is good because: -- Requires filtering bugs by date, priority, and status -- Needs to group by assignee and calculate resolution rates -- Requires understanding timestamps to determine 48-hour windows -- Tests pagination (potentially many bugs to process) -- Answer is a single username -- Based on historical data from specific time period - -**Example 4: Requires synthesis across multiple data types (CRM MCP)** -```xml - - Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in? - Healthcare - -``` - -This question is good because: -- Requires understanding subscription tier changes -- Needs to identify upgrade events in specific timeframe -- Requires comparing contract values -- Must access account industry information -- Answer is simple and verifiable -- Based on completed historical transactions - -### Poor Questions - -**Example 1: Answer changes over time** -```xml - - How many open issues are currently assigned to the engineering team? - 47 - -``` - -This question is poor because: -- The answer will change as issues are created, closed, or reassigned -- Not based on stable/stationary data -- Relies on "current state" which is dynamic - -**Example 2: Too easy with keyword search** -```xml - - Find the pull request with title "Add authentication feature" and tell me who created it. - developer123 - -``` - -This question is poor because: -- Can be solved with a straightforward keyword search for exact title -- Doesn't require deep exploration or understanding -- No synthesis or analysis needed - -**Example 3: Ambiguous answer format** -```xml - - List all the repositories that have Python as their primary language. - repo1, repo2, repo3, data-pipeline, ml-tools - -``` - -This question is poor because: -- Answer is a list that could be returned in any order -- Difficult to verify with direct string comparison -- LLM might format differently (JSON array, comma-separated, newline-separated) -- Better to ask for a specific aggregate (count) or superlative (most stars) - -## Verification Process - -After creating evaluations: - -1. **Examine the XML file** to understand the schema -2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF -3. **Flag any operations** that require WRITE or DESTRUCTIVE operations -4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document -5. **Remove any ``** that require WRITE or DESTRUCTIVE operations - -Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end. - -## Tips for Creating Quality Evaluations - -1. **Think Hard and Plan Ahead** before generating tasks -2. **Parallelize Where Opportunity Arises** to speed up the process and manage context -3. **Focus on Realistic Use Cases** that humans would actually want to accomplish -4. **Create Challenging Questions** that test the limits of the MCP server's capabilities -5. **Ensure Stability** by using historical data and closed concepts -6. **Verify Answers** by solving the questions yourself using the MCP server tools -7. **Iterate and Refine** based on what you learn during the process - ---- - -# Running Evaluations - -After creating your evaluation file, you can use the provided evaluation harness to test your MCP server. - -## Setup - -1. **Install Dependencies** - - ```bash - pip install -r scripts/requirements.txt - ``` - - Or install manually: - ```bash - pip install anthropic mcp - ``` - -2. **Set API Key** - - ```bash - export ANTHROPIC_API_KEY=your_api_key_here - ``` - -## Evaluation File Format - -Evaluation files use XML format with `` elements: - -```xml - - - Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name? - Website Redesign - - - Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username. - sarah_dev - - -``` - -## Running Evaluations - -The evaluation script (`scripts/evaluation.py`) supports three transport types: - -**Important:** -- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually. -- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL. - -### 1. Local STDIO Server - -For locally-run MCP servers (script launches the server automatically): - -```bash -python scripts/evaluation.py \ - -t stdio \ - -c python \ - -a my_mcp_server.py \ - evaluation.xml -``` - -With environment variables: -```bash -python scripts/evaluation.py \ - -t stdio \ - -c python \ - -a my_mcp_server.py \ - -e API_KEY=abc123 \ - -e DEBUG=true \ - evaluation.xml -``` - -### 2. Server-Sent Events (SSE) - -For SSE-based MCP servers (you must start the server first): - -```bash -python scripts/evaluation.py \ - -t sse \ - -u https://example.com/mcp \ - -H "Authorization: Bearer token123" \ - -H "X-Custom-Header: value" \ - evaluation.xml -``` - -### 3. HTTP (Streamable HTTP) - -For HTTP-based MCP servers (you must start the server first): - -```bash -python scripts/evaluation.py \ - -t http \ - -u https://example.com/mcp \ - -H "Authorization: Bearer token123" \ - evaluation.xml -``` - -## Command-Line Options - -``` -usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND] - [-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL] - [-H HEADERS [HEADERS ...]] [-o OUTPUT] - eval_file - -positional arguments: - eval_file Path to evaluation XML file - -optional arguments: - -h, --help Show help message - -t, --transport Transport type: stdio, sse, or http (default: stdio) - -m, --model Claude model to use (default: claude-3-7-sonnet-20250219) - -o, --output Output file for report (default: print to stdout) - -stdio options: - -c, --command Command to run MCP server (e.g., python, node) - -a, --args Arguments for the command (e.g., server.py) - -e, --env Environment variables in KEY=VALUE format - -sse/http options: - -u, --url MCP server URL - -H, --header HTTP headers in 'Key: Value' format -``` - -## Output - -The evaluation script generates a detailed report including: - -- **Summary Statistics**: - - Accuracy (correct/total) - - Average task duration - - Average tool calls per task - - Total tool calls - -- **Per-Task Results**: - - Prompt and expected response - - Actual response from the agent - - Whether the answer was correct (✅/❌) - - Duration and tool call details - - Agent's summary of its approach - - Agent's feedback on the tools - -### Save Report to File - -```bash -python scripts/evaluation.py \ - -t stdio \ - -c python \ - -a my_server.py \ - -o evaluation_report.md \ - evaluation.xml -``` - -## Complete Example Workflow - -Here's a complete example of creating and running an evaluation: - -1. **Create your evaluation file** (`my_evaluation.xml`): - -```xml - - - Find the user who created the most issues in January 2024. What is their username? - alice_developer - - - Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name. - backend-api - - - Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take? - 127 - - -``` - -2. **Install dependencies**: - -```bash -pip install -r scripts/requirements.txt -export ANTHROPIC_API_KEY=your_api_key -``` - -3. **Run evaluation**: - -```bash -python scripts/evaluation.py \ - -t stdio \ - -c python \ - -a github_mcp_server.py \ - -e GITHUB_TOKEN=ghp_xxx \ - -o github_eval_report.md \ - my_evaluation.xml -``` - -4. **Review the report** in `github_eval_report.md` to: - - See which questions passed/failed - - Read the agent's feedback on your tools - - Identify areas for improvement - - Iterate on your MCP server design - -## Troubleshooting - -### Connection Errors - -If you get connection errors: -- **STDIO**: Verify the command and arguments are correct -- **SSE/HTTP**: Check the URL is accessible and headers are correct -- Ensure any required API keys are set in environment variables or headers - -### Low Accuracy - -If many evaluations fail: -- Review the agent's feedback for each task -- Check if tool descriptions are clear and comprehensive -- Verify input parameters are well-documented -- Consider whether tools return too much or too little data -- Ensure error messages are actionable - -### Timeout Issues - -If tasks are timing out: -- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`) -- Check if tools are returning too much data -- Verify pagination is working correctly -- Consider simplifying complex questions \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java index ef21f7070..2ddbc1c25 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/adapter/sender/EaiEimsSender.java @@ -58,7 +58,7 @@ public class EaiEimsSender implements EimsSender { log.warn(" 로컬 환경이거나 Ka\n" + "\n" + "
\n" + - " MCP_WAS_ASYNC\n" + + " MCP_GATEWAY_ASYNC\n" + " EAI_BATCH_JOB\n" + " 1782705901850\n" + " ASYNC\n" + @@ -91,7 +91,7 @@ public class EaiEimsSender implements EimsSender { return String.format( "" + "
" + - "MCP_WAS_ASYNC" + + "MCP_GATEWAY_ASYNC" + "%s" + "%d" + "ASYNC" + diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java deleted file mode 100644 index cdfc10617..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpAnyOf.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpAnyOf - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * MCP 스키마 생성 시 anyOf (해당 필드들 중 최소 1개 이상 필수) 제약을 부여합니다. - */ -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -public @interface McpAnyOf { - /** - * anyOf 제약에 포함될 필드명 목록 - * 예: @McpAnyOf({"claimNo", "contractNo"}) - */ - String[] value(); -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java deleted file mode 100644 index aa0d6aeba..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpFunction - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import java.lang.annotation.*; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpFunction { - String displayName(); // 사람이 읽는 라벨 (예: "고객 조회 툴") - String name(); // MCP 서브툴 명칭 (예: "customer_search") - String description(); - String prompt() default ""; - String mappingId() default ""; - - - /** - * Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다. - */ - String inputSchema() default "{}"; - - /** - * 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다. - * inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다. - */ - // 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어 - String inputSchemaResource() default ""; - - /** - * Tool response JSON Schema. When unset, output validation is skipped. - */ - String outputSchema() default "{}"; - - /** - * Classpath resource for a complex Tool response JSON Schema. - * This value has priority over outputSchema. - */ - String outputSchemaResource() default ""; - boolean register() default false; - - // 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김) - boolean visible() default true; - - // 추가: HITL 승인 체계 지원 (실행 전 사용자 승인 필요 여부) - boolean requiresApproval() default false; - - boolean readOnlyHint() default false; - boolean destructiveHint() default false; - boolean idempotentHint() default false; - boolean openWorldHint() default false; - - /** Version exposed as _meta.version in the Tool Manifest. */ - String version() default "1.0.0"; - - /** Maximum execution time exposed as _meta.timeoutMillis in the Tool Manifest. */ - long timeoutMillis() default 300000L; - - /** Whether the Tool is available for MCP exposure. */ - boolean enabled() default true; - - // 추가: 툴 별 기본 Timeout 설정 (기본 300초 = 300000ms) -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java deleted file mode 100644 index 49270784c..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpOutputSchema - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a Tool response DTO for automatic output JSON Schema generation. - * Field constraints are declared with {@link McpValidation}. - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpOutputSchema { -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java deleted file mode 100644 index 0497a02a3..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpParameter.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpParameter - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import java.lang.annotation.*; - -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpParameter { - String description(); - boolean required() default false; -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java deleted file mode 100644 index 340177865..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpTool.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpTool - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import org.springframework.core.annotation.AliasFor; -import org.springframework.stereotype.Component; - -import java.lang.annotation.*; - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Component -public @interface McpTool { - @AliasFor(annotation = Component.class) - String value() default ""; - - String categoryKey() default "common"; - String routingType() default "HTTP"; -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java deleted file mode 100644 index 0def3e577..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.shinhanlife.dap.lib.annotation; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpValidation - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * @package io.shinhanlife.dap.lib.annotation - * @className McpValidation - * @description Declares JSON Schema validation constraints for MCP tool input fields - * @author 0986406 - * @create 2026.07.27 - *
- * ---------- revision history ----------
- * date       author    description
- * ---------- --------- ---------------------------
- * 2026.07.27 0986406    initial creation
- * 
- */ -@Target(ElementType.FIELD) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface McpValidation { - boolean required() default false; - String pattern() default ""; - long minimum() default Long.MIN_VALUE; - long maximum() default Long.MAX_VALUE; - int minLength() default -1; - int maxLength() default -1; - String[] allowedValues() default {}; - String format() default ""; - boolean nullable() default false; String defaultValue() default ""; - String[] examples() default {}; -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java new file mode 100644 index 000000000..a0586fb16 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation/ToolHint.java @@ -0,0 +1,32 @@ +package io.shinhanlife.dap.lib.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Spring AI @Tool 어노테이션을 보완하여 MCP 시스템 메타데이터를 추가 제공하는 힌트 어노테이션 + * @package io.shinhanlife.dap.lib.annotation + * @className ToolHint + * @description 비즈니스 로직(Tool)과 시스템 제어 메타데이터 분리 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ToolHint { + boolean register() default false; + boolean requiresApproval() default false; + String categoryKey() default "com"; + String mappingId() default ""; + String inputSchemaResource() default ""; + String outputSchemaResource() default ""; +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java index a7aef5eb3..7408c57df 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/aop/ToolSlaMonitoringAspect.java @@ -15,7 +15,6 @@ package io.shinhanlife.dap.lib.aop; * * */ -import io.shinhanlife.dap.lib.annotation.McpFunction; import io.shinhanlife.dap.lib.config.McpProperties; import java.lang.reflect.Method; import lombok.RequiredArgsConstructor; @@ -24,6 +23,7 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.ai.mcp.annotation.McpTool; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; @@ -35,12 +35,12 @@ public class ToolSlaMonitoringAspect { private final McpProperties mcpProperties; - // @McpFunction 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다. - @Around("@annotation(McpFunction)") + // @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다. + @Around("@annotation(org.springframework.ai.mcp.annotation.McpTool)") public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); - McpFunction functionAnnotation = method.getAnnotation(McpFunction.class); + McpTool functionAnnotation = method.getAnnotation(McpTool.class); // 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다. String baseName = functionAnnotation.name(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java index d507f890e..0364a5e59 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.config; -/** - * @package io.shinhanlife.dap.lib.config - * @className ToolSchemaConfiguration - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import org.springframework.context.annotation.Bean; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java new file mode 100644 index 000000000..6d8684007 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/dto/OperationType.java @@ -0,0 +1,20 @@ +package io.shinhanlife.dap.lib.dto; + +/** + * @package io.shinhanlife.dap.mcg.dto + * @className OperationType + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public enum OperationType { + READ, + WRITE +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java index 6ad8defa5..f793904ec 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestAnnotations.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestAnnotations - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Behaviour hints exposed by the Tool Service manifest. */ public record ToolManifestAnnotations( String title, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java index dece89006..f03c9a96d 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestItem.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestItem - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Map; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java index 2021c8bc7..192ba496a 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestMeta.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestMeta - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Operational metadata exposed by the Tool Service manifest. */ public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) { } \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java index 8284c0be3..293a9e01b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestResponse.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestResponse - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.util.List; /** Top-level response for GET /tool-manifest. */ diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java index 44f0eeef8..5050a442a 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest/ToolManifestService.java @@ -1,25 +1,9 @@ package io.shinhanlife.dap.lib.manifest; -/** - * @package io.shinhanlife.dap.lib.manifest - * @className ToolManifestService - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.mcp.LocalToolScanner; +import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.Comparator; @@ -46,9 +30,9 @@ public class ToolManifestService { private String lastRevision; @Autowired - public ToolManifestService(LocalToolScanner localToolScanner, ObjectMapper objectMapper, + public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper, McpProperties properties) { - this(localToolScanner::getAllScannedTools, objectMapper, properties); + this(heartbeatSender::getAllScannedTools, objectMapper, properties); } ToolManifestService(Supplier> toolSupplier, ObjectMapper objectMapper, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java deleted file mode 100644 index 183bd61c7..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/LocalToolScanner.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.shinhanlife.dap.lib.mcp; - - -/** - * @package io.shinhanlife.dap.mcc.service - * @className LocalToolScanner - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import com.fasterxml.jackson.databind.ObjectMapper; -import io.shinhanlife.dap.lib.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpTool; -import io.shinhanlife.dap.lib.config.McpProperties; -import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.util.ToolSchemaResolver; -import jakarta.annotation.PostConstruct; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.aop.support.AopUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.stereotype.Component; -import org.springframework.util.ClassUtils; - -@Slf4j -@Component -@Configuration -@RequiredArgsConstructor -public class LocalToolScanner { - - private final ApplicationContext applicationContext; - private final ObjectMapper objectMapper; - private final McpProperties mcpProperties; - private final ToolSchemaResolver toolSchemaResolver; - - @Value("${axhub.tool.url:http://localhost:8080}") - private String podUrl; - - private List registeredTools = new ArrayList<>(); - - @Getter - private List allScannedTools = new ArrayList<>(); - - @PostConstruct - public void init() { - log.info(" [LocalToolScanner] 초기화 시작. Pod URL: {}", podUrl); - scanAndBuildMetadata(); - } - - private void scanAndBuildMetadata() { - Map allBeans = applicationContext.getBeansOfType(Object.class); - for (Object bean : allBeans.values()) { - Class targetClass = AopUtils.getTargetClass(bean); - - // 클래스 또는 프록시(인터페이스)에서 @McpTool 스캔 - McpTool toolAnnotation = AnnotationUtils.findAnnotation(targetClass, McpTool.class); - if (toolAnnotation == null) { - toolAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), McpTool.class); - } - - for (Method method : targetClass.getDeclaredMethods()) { - // 메서드, 수퍼클래스, 인터페이스를 모두 뒤져서 @McpFunction 스캔 - McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class); - - if (functionAnnotation != null && toolAnnotation != null) { - String baseName = functionAnnotation.displayName(); - String rawSubToolName = functionAnnotation.name(); - String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() - ? mcpProperties.getNamespace() + "_" + rawSubToolName - : rawSubToolName; - - boolean isRegister = functionAnnotation.register(); - if (!isRegister) { - log.info(" [LocalToolScanner] '{}' 툴은 어노테이션 설정에 의해 제외되었습니다. (최종 이름: {})", baseName, subToolName); - } - - ToolMetadata meta = new ToolMetadata(); - meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString()); - meta.setDisplayName(baseName); - meta.setName(subToolName); - meta.setSemver(functionAnnotation.version()); - meta.setTimeoutMillis(functionAnnotation.timeoutMillis()); - meta.setEnabled(functionAnnotation.enabled()); - meta.setDescription(functionAnnotation.description()); - meta.setCategoryKey(toolAnnotation.categoryKey()); - meta.setIntegrationType(toolAnnotation.routingType()); - meta.setMciServiceId(functionAnnotation.mappingId()); - meta.setPodUrl(podUrl); - meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName); - - boolean isVisible = functionAnnotation.visible(); - meta.setVisible(isVisible); - meta.setIsRegistered(isRegister); - meta.setRequiresApproval(functionAnnotation.requiresApproval()); - meta.setReadOnlyHint(functionAnnotation.readOnlyHint()); - meta.setDestructiveHint(functionAnnotation.destructiveHint()); - meta.setIdempotentHint(functionAnnotation.idempotentHint()); - meta.setOpenWorldHint(functionAnnotation.openWorldHint()); - - Map prompts = new HashMap<>(); - String promptText = functionAnnotation.prompt(); - prompts.put(subToolName, promptText); - meta.setActionPrompts(prompts); - - if (method.getParameterCount() > 0) { - try { - Class paramType = method.getParameterTypes()[0]; - Map finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType); - meta.setParametersSchema(finalSchema); - } catch (Exception e) { - log.error("Failed to generate schema for {}", subToolName, e); - } - } - - if (isRegister) { - registeredTools.add(meta); - } - allScannedTools.add(meta); - log.info(" [LocalToolScanner] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister); - } - } - } - } - -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java index a96608926..0104394df 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderContext.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className McpRequestHeaderContext - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Holds optional MCP headers for the lifetime of one HTTP request thread. */ public final class McpRequestHeaderContext { private static final ThreadLocal CURRENT_HEADERS = new ThreadLocal<>(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java index b90a67c35..aae2654f4 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className McpRequestHeaderFilter - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.io.IOException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java index b88a6698c..3eb87c766 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className McpRequestHeaders - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - /** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */ public record McpRequestHeaders( String headerRequestId, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java index 50713588d..90afd71bd 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolMcpServerConfiguration.java @@ -1,31 +1,25 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className ToolMcpServerConfiguration - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; /** Exposes every Tool Pod through the MCP Streamable HTTP transport. */ +@Slf4j @Configuration public class ToolMcpServerConfiguration { + @PostConstruct + public void init() { + log.warn("================================================="); + log.warn("ToolMcpServerConfiguration IS LOADED BY SPRING!!!"); + log.warn("================================================="); + } + @Bean @Primary public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() { @@ -37,6 +31,8 @@ public class ToolMcpServerConfiguration { @Bean public ServletRegistrationBean toolMcpServlet( HttpServletStreamableServerTransportProvider transportProvider) { - return new ServletRegistrationBean<>(transportProvider, "/mcp"); + // "/mcp/*"로 매핑하면 BusinessToolController의 "/mcp/api/v1/tools/local" 까지 가로채게 되므로, + // 정확히 MCP 통신에 사용되는 "/mcp" 와 "/mcp/message" 두 개만 매핑합니다. + return new ServletRegistrationBean<>(transportProvider, "/mcp", "/mcp/message"); } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java index 105337870..d9f23f019 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolPodMcpToolSynchronizer.java @@ -1,28 +1,14 @@ package io.shinhanlife.dap.lib.mcp; -/** - * @package io.shinhanlife.dap.lib.mcp - * @className ToolPodMcpToolSynchronizer - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.spec.McpSchema; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.presentation.BusinessToolController; -import io.shinhanlife.dap.lib.mcp.LocalToolScanner; +import io.shinhanlife.dap.mcc.presentation.BusinessToolController; +import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.boot.context.event.ApplicationReadyEvent; @@ -30,25 +16,28 @@ import org.springframework.context.event.EventListener; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; + /** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */ @Component +@ConditionalOnBean(BusinessToolController.class) public class ToolPodMcpToolSynchronizer { private final McpSyncServer mcpServer; - private final LocalToolScanner localToolScanner; + private final ToolRegistryHeartbeatSender heartbeatSender; private final BusinessToolController businessToolController; private final ObjectMapper objectMapper; - public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, LocalToolScanner localToolScanner, + public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender, BusinessToolController businessToolController, ObjectMapper objectMapper) { this.mcpServer = mcpServer; - this.localToolScanner = localToolScanner; + this.heartbeatSender = heartbeatSender; this.businessToolController = businessToolController; this.objectMapper = objectMapper; } @EventListener(ApplicationReadyEvent.class) public void registerLocalTools() { - localToolScanner.getAllScannedTools().stream() + heartbeatSender.getAllScannedTools().stream() .filter(tool -> Boolean.TRUE.equals(tool.getVisible())) .forEach(tool -> mcpServer.addTool(specification(tool))); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java new file mode 100644 index 000000000..829655989 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/ToolRegistryHeartbeatSender.java @@ -0,0 +1,197 @@ +package io.shinhanlife.dap.lib.mcp; + + +/** + * @package io.shinhanlife.dap.mcc.service + * @className ToolRegistryHeartbeatSender + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.ai.mcp.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.lib.config.McpProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import io.shinhanlife.dap.lib.util.ToolSchemaResolver; +import jakarta.annotation.PostConstruct; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; +import org.springframework.web.client.RestClient; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import io.shinhanlife.dap.mcc.presentation.BusinessToolController; + +@Slf4j +@Component +@Configuration +@EnableScheduling +@RequiredArgsConstructor +@ConditionalOnBean(BusinessToolController.class) +public class ToolRegistryHeartbeatSender { + + private final ApplicationContext applicationContext; + private final ObjectMapper objectMapper; + private final McpProperties mcpProperties; + private final RestClient restClient = RestClient.create(); + private final ToolSchemaResolver toolSchemaResolver; + + @Value("${axhub.gateway.url:http://localhost:8081}") + private String gatewayUrl; + + @Value("${axhub.tool.url:http://localhost:8080}") + private String podUrl; + + private List registeredTools = new ArrayList<>(); + + @Getter + private List allScannedTools = new ArrayList<>(); + + @PostConstruct + public void init() { + log.info(" [HeartbeatSender] 초기화 시작. Gateway URL: {}, Pod URL: {}", gatewayUrl, podUrl); + scanAndBuildMetadata(); + } + + private void scanAndBuildMetadata() { + Map allBeans = applicationContext.getBeansOfType(Object.class); + for (Object bean : allBeans.values()) { + Class targetClass = AopUtils.getTargetClass(bean); + + for (Method method : targetClass.getDeclaredMethods()) { + McpTool functionAnnotation = AnnotationUtils.findAnnotation(method, McpTool.class); + ToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, ToolHint.class); + + if (functionAnnotation != null) { + String baseName = functionAnnotation.name(); + String rawSubToolName = functionAnnotation.name(); + String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() + ? mcpProperties.getNamespace() + "_" + rawSubToolName + : rawSubToolName; + + boolean isRegister = hintAnnotation != null && hintAnnotation.register(); + if (!isRegister) { + log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName); + } + + ToolMetadata meta = new ToolMetadata(); + meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString()); + String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title(); + meta.setDisplayName(displayName); + meta.setName(subToolName); + meta.setSemver("1.0.0"); + meta.setTimeoutMillis(5000L); + meta.setEnabled(true); + meta.setDescription(functionAnnotation.description()); + meta.setCategoryKey(hintAnnotation == null || hintAnnotation.categoryKey().isBlank() + ? "common" : hintAnnotation.categoryKey()); + meta.setIntegrationType("REST"); + meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : ""); + meta.setPodUrl(podUrl); + meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName); + + meta.setVisible(true); + meta.setIsRegistered(isRegister); + meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval()); + + // extract standard hints from @McpTool.annotations() + McpTool.McpAnnotations ann = functionAnnotation.annotations(); + if (ann != null) { + meta.setReadOnlyHint(ann.readOnlyHint()); + meta.setDestructiveHint(ann.destructiveHint()); + meta.setIdempotentHint(ann.idempotentHint()); + meta.setOpenWorldHint(ann.openWorldHint()); + } else { + meta.setReadOnlyHint(false); + meta.setDestructiveHint(true); + meta.setIdempotentHint(false); + meta.setOpenWorldHint(true); + } + + Map prompts = new HashMap<>(); + meta.setActionPrompts(prompts); + + if (method.getParameterCount() > 0) { + try { + Class paramType = method.getParameterTypes()[0]; + // TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction + Map finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType); + meta.setParametersSchema(finalSchema); + } catch (Exception e) { + log.error("Failed to generate schema for {}", subToolName, e); + } + } + + if (isRegister) { + registeredTools.add(meta); + } + allScannedTools.add(meta); + log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister); + } + } + } + } + + @Scheduled(fixedRate = 30000) + public void sendHeartbeats() { + if (registeredTools.isEmpty()) return; + + for (ToolMetadata tool : registeredTools) { + try { + ResponseEntity response = restClient.post() + .uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat") + .header("Content-Type", "application/json") + + .body(tool.getUid()) + .retrieve() + .toEntity(String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid()); + } + } catch (Exception e) { + log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage()); + registerTool(tool); + } + } + } + + private void registerTool(ToolMetadata tool) { + try { + restClient.post() + .uri(gatewayUrl + "/mcp/api/v1/registry/register") + .header("Content-Type", "application/json") + + .body(tool) + .retrieve() + .toBodilessEntity(); + log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid()); + } catch (Exception ex) { + log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage()); + } + } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java index 26f5f67d8..5c2e22c96 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/SwaggerConfig.java @@ -29,11 +29,11 @@ public class SwaggerConfig { public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() - .title("Shinhan MCP WAS API 명세서") + .title("Shinhan MCP Gateway API 명세서") .version("v1.0") - .description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter WAS API 문서입니다.")) + .description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다.")) .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)")) - .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("WAS Pod")) + .addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)")) // 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다. .addSecurityItem(new SecurityRequirement().addList("X-API-KEY")) .components(new Components() diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java index 8bfbcc2fb..9e3cfc8b4 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java @@ -43,7 +43,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { - // Swagger UI(8080)에서 WAS로 API 호출 시 발생하는 CORS 에러 해결 + // Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결 registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java index 065d8012e..f5700193f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/exception/GlobalExceptionHandler.java @@ -29,25 +29,25 @@ public class GlobalExceptionHandler { @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity handleNoResourceFound(NoResourceFoundException e) { - log.warn(" [WAS Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath()); + log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath()); return ResponseEntity.notFound().build(); } @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgument(IllegalArgumentException e) { - log.warn(" [WAS Bad Request] 잘못된 요청: {}", e.getMessage()); + log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage()); return buildErrorResponse(-32602, "Invalid params: " + e.getMessage()); } @ExceptionHandler(RuntimeException.class) public ResponseEntity handleRuntime(RuntimeException e) { - log.error(" [WAS Internal Error] 시스템 장애: {}", e.getMessage(), e); + log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e); return buildErrorResponse(-32603, "Internal error: " + e.getMessage()); } @ExceptionHandler(Exception.class) public ResponseEntity handleAllException(Exception e) { - log.error(" [WAS Fatal Error] 치명적 오류 발생", e); + log.error(" [Gateway Fatal Error] 치명적 오류 발생", e); return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요."); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/BusinessToolController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/BusinessToolController.java deleted file mode 100644 index 4ca3f6d63..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/BusinessToolController.java +++ /dev/null @@ -1,244 +0,0 @@ -package io.shinhanlife.dap.lib.presentation; - - -/** - * @package io.shinhanlife.dap.mcc.presentation - * @className BusinessToolController - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.Error; -import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; -import io.shinhanlife.dap.lib.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpTool; -import io.shinhanlife.dap.lib.config.McpProperties; -import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.mcp.LocalToolScanner; -import io.shinhanlife.dap.lib.util.ToolSchemaResolver; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.aop.support.AopUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.http.ResponseEntity; -import org.springframework.util.ClassUtils; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequestMapping("/") -@RequiredArgsConstructor -public class BusinessToolController { - - private final ApplicationContext applicationContext; - private final ObjectMapper objectMapper; - private final McpProperties mcpProperties; - private final LocalToolScanner localToolScanner; - private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; - - private final ToolSchemaResolver toolSchemaResolver; - // 내부 조회용 로컬 Tool 목록 엔드포인트 - @GetMapping("/mcp/api/v1/tools/local") - public List getLocalTools() { - return localToolScanner.getAllScannedTools(); - } - - // 순수 REST 기반 동적 라우팅 엔드포인트 - @PostMapping("/mcp/{name}") - public ResponseEntity executeDynamicTool( - @PathVariable("name") String functionName, - @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId, - @RequestHeader(value = "trace-id", required = false) String traceId, - @RequestHeader(value = "request-id", required = false) String requestId, - @RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId, - @RequestBody(required = false) Map arguments) { - - String finalRequestId = headerRequestId; - - log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId); - log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); - if (arguments != null) { - try { - log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments)); - } catch (Exception e) { - log.info(" [Tool] 호출 파라미터: {}", arguments); - } - } - Object targetBean = null; - Method targetMethod = null; - McpFunction targetFunctionAnnotation = null; - - // McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpFunction을 찾습니다. - Map allBeans = applicationContext.getBeansOfType(Object.class); - outerLoop: - for (Object bean : allBeans.values()) { - Class targetClass = AopUtils.getTargetClass(bean); - for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) { - McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class); - if (mcpFunc != null) { - String baseName = mcpFunc.name(); - String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() - ? mcpProperties.getNamespace() + "_" + baseName - : baseName; - - if (expectedName.equals(functionName) || baseName.equals(functionName)) { - targetBean = bean; - try { - targetMethod = bean.getClass().getMethod(targetMethodOfClass.getName(), targetMethodOfClass.getParameterTypes()); - } catch (NoSuchMethodException e) { - targetMethod = targetMethodOfClass; - } - targetFunctionAnnotation = mcpFunc; - break outerLoop; - } - } - } - } - - if (targetBean == null || targetMethod == null) { - List availableFunctions = new ArrayList<>(); - for (Object bean : allBeans.values()) { - Class targetCls = AopUtils.getTargetClass(bean); - for (Method m : targetCls.getDeclaredMethods()) { - McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class); - if (func != null) { - String baseName = func.name(); - String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() - ? mcpProperties.getNamespace() + "_" + baseName : baseName; - availableFunctions.add(expName + " (in " + targetCls.getSimpleName() + ")"); - } - } - } - log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions); - Map errorDetails = new HashMap<>(); - errorDetails.put("status", "404"); - Map errorBody = new HashMap<>(); - errorBody.put("code", "TOOL_NOT_FOUND"); - errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName); - errorBody.put("details", errorDetails); - if (finalRequestId != null) errorBody.put("request_id", finalRequestId); - - return ResponseEntity.status(404).body(errorBody); - } - // (기존 차단 로직 제거됨) - - // 2. 파라미터 유효성 검증 (JSON Schema) - if (targetMethod.getParameterCount() > 0) { - Class paramType = targetMethod.getParameterTypes()[0]; - if (!Map.class.isAssignableFrom(paramType)) { - try { - Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType); - List errors = toolArgumentSchemaValidator.validate(inputSchema, arguments); - if (!errors.isEmpty()) { - log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); - List errorMessages = new ArrayList<>(); - for (Error validationError : errors) { - errorMessages.add(validationError.getMessage()); - } - Map errorDetails = new HashMap<>(); - errorDetails.put("status", "422"); - Map errorBody = new HashMap<>(); - errorBody.put("code", "INVALID_PARAM"); - errorBody.put("message", "파라미터 유효성 검증 실패"); - errorBody.put("details", errorDetails); - if (finalRequestId != null) errorBody.put("request_id", finalRequestId); - - return ResponseEntity.status(422).body(errorBody); - } - } catch (Exception e) { - log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); - } - } - } - - log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName()); - - try { - // 3. DTO 파라미터 자동 매핑 (Map -> DTO) - Object invokeArgument = arguments; - if (targetMethod.getParameterCount() > 0 && arguments != null) { - Class paramType = targetMethod.getParameterTypes()[0]; - if (!Map.class.isAssignableFrom(paramType)) { - invokeArgument = objectMapper.convertValue(arguments, paramType); - log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName()); - } - } - - long startTime = System.currentTimeMillis(); - Object methodResult = null; - if (targetMethod.getParameterCount() == 0) { - methodResult = targetMethod.invoke(targetBean); - } else { - methodResult = targetMethod.invoke(targetBean, invokeArgument); - } - - Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType()); - if (!outputSchema.isEmpty()) { - List outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult); - if (!outputErrors.isEmpty()) { - log.error("[Tool] Output schema validation failed. tool={}, errors={}", - functionName, outputErrors); - Map errorBody = new HashMap<>(); - errorBody.put("code", "INVALID_TOOL_RESPONSE"); - errorBody.put("message", "Tool response does not match its output schema"); - if (finalRequestId != null) { - errorBody.put("request_id", finalRequestId); - } - return ResponseEntity.internalServerError().body(errorBody); - } - } - - long elapsed = System.currentTimeMillis() - startTime; - - // 5. 결과 반환 (순수 REST 응답) - try { - log.info("[Tool Execution] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); - } catch (Exception e) { - log.info("[Tool Execution] Output Schema Result: {}", methodResult); - } - - log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId); - - ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok(); - if (traceId != null) responseBuilder.header("trace-id", traceId); - if (requestId != null) responseBuilder.header("request-id", requestId); - - return responseBuilder.body(methodResult); - - } catch (Exception e) { - log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); - Map errorDetails = new HashMap<>(); - errorDetails.put("status", "500"); - Map errorBody = new HashMap<>(); - errorBody.put("code", "TOOL_ERROR"); - errorBody.put("message", "Tool execution failed"); - errorDetails.clear(); // Hide details for upstream errors - errorBody.put("details", errorDetails); - if (finalRequestId != null) errorBody.put("request_id", finalRequestId); - - return ResponseEntity.status(502).body(errorBody); - } - } -} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/ToolManifestController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/ToolManifestController.java deleted file mode 100644 index 057a68acb..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/presentation/ToolManifestController.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.shinhanlife.dap.lib.presentation; - -/** - * @package io.shinhanlife.dap.lib.presentation - * @className ToolManifestController - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - -import io.shinhanlife.dap.lib.manifest.ToolManifestResponse; -import io.shinhanlife.dap.lib.manifest.ToolManifestService; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RestController; - -/** Read-only Tool Service manifest endpoint for MCP background discovery. */ -@RestController -public class ToolManifestController { - - private final ToolManifestService toolManifestService; - - public ToolManifestController(ToolManifestService toolManifestService) { - this.toolManifestService = toolManifestService; - } - - @GetMapping(value = "/tool-manifest", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getManifest( - @RequestHeader(value = HttpHeaders.IF_NONE_MATCH, required = false) String ifNoneMatch) { - ToolManifestResponse manifest = toolManifestService.currentManifest(); - String eTag = '"' + manifest.revision() + '"'; - if (eTag.equals(ifNoneMatch)) { - return ResponseEntity.status(304).eTag(eTag).build(); - } - return ResponseEntity.ok().eTag(eTag).body(manifest); - } -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java deleted file mode 100644 index a440f3957..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/SsoRestController.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.shinhanlife.dap.lib.session.presentation; - -import io.micrometer.common.util.StringUtils; -import io.shinhanlife.glow.BaseResponse; -import io.shinhanlife.glow.BizException; -import io.shinhanlife.glow.GlowControllerId; -import io.shinhanlife.glow.ResponseUtil; -import io.shinhanlife.dap.lib.session.dto.SessionDto; -import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.apache.ibatis.javassist.NotFoundException; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -/** - * @package io.shinhanlife.dap.lib.session.presentation - * @className SsoRestController - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -@RestController -@RequiredArgsConstructor -@Slf4j -@RequestMapping("/sso") -public class SsoRestController { - - private static final String NLS_LOGIN_URL = ""; - - /** - * sso 연동 전 임시 로그인 - * - * @param request - * @param response - * @param session - * @param - * @return - */ - @GlowControllerId("tempLogin") - @PostMapping("/tempLogin") - public ResponseEntity> tempLogin(HttpServletRequest request, HttpServletResponse response, - HttpSession session, @RequestBody SessionDto requestDto) { - try { - if (StringUtils.isEmpty(requestDto.getPrafNo())) { - throw new NotFoundException("SSO >> not found sso id"); - } - - // DB 조회(ZtUsac) 없이 파라미터로 받은 SessionDto를 그대로 사용 - SessionDto sessionDto = requestDto; - sessionDto.setLoginDtm(); // 로그인 시점 세팅 - session.setAttribute("userInfo", sessionDto); - return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build()); - - } catch (Exception e) { - log.error("로그인 실패", e); - session.invalidate(); - } - - return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build()); - } -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java deleted file mode 100644 index 10594ebd4..000000000 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/session/presentation/io/SsoResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.shinhanlife.dap.lib.session.presentation.io; - -import io.shinhanlife.dap.lib.session.dto.SessionDto; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -/** - * @package io.shinhanlife.dap.lib.session.presentation.io - * @className SsoResponse - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SsoResponse { - - private String retCode; - private SessionDto userInfo; - private String redirectUrl; - -} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java index d95dae6e3..1e4938049 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java @@ -2,9 +2,8 @@ package io.shinhanlife.dap.lib.util; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyDescription; -import io.shinhanlife.dap.lib.annotation.McpParameter; -import io.shinhanlife.dap.lib.annotation.McpValidation; -import io.shinhanlife.dap.lib.annotation.McpAnyOf; +import org.springframework.ai.mcp.annotation.McpToolParam; +import io.swagger.v3.oas.annotations.media.Schema; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -56,7 +55,7 @@ public class JsonSchemaGenerator { // 1. 타입 매핑 // 2. 어노테이션 기반 설명 추출 - McpParameter paramAnnotation = field.getAnnotation(McpParameter.class); + McpToolParam paramAnnotation = field.getAnnotation(McpToolParam.class); JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class); if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) { fieldSchema.put("description", paramAnnotation.description()); @@ -72,46 +71,54 @@ public class JsonSchemaGenerator { requiredList.add(field.getName()); } - McpValidation validation = field.getAnnotation(McpValidation.class); - if (validation != null && validation.required() && !requiredList.contains(field.getName())) { - requiredList.add(field.getName()); + Schema schemaAnnotation = field.getAnnotation(Schema.class); + if (schemaAnnotation != null) { + if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) { + fieldSchema.put("description", schemaAnnotation.description()); + } + if (schemaAnnotation.required() && !requiredList.contains(field.getName())) { + requiredList.add(field.getName()); + } + if (!schemaAnnotation.pattern().isEmpty()) { + fieldSchema.put("pattern", schemaAnnotation.pattern()); + } + if (!schemaAnnotation.minimum().isEmpty()) { + try { + fieldSchema.put("minimum", Long.valueOf(schemaAnnotation.minimum())); + } catch (NumberFormatException ignored) {} + } + if (!schemaAnnotation.maximum().isEmpty()) { + try { + fieldSchema.put("maximum", Long.valueOf(schemaAnnotation.maximum())); + } catch (NumberFormatException ignored) {} + } + if (schemaAnnotation.minLength() > 0) { + fieldSchema.put("minLength", schemaAnnotation.minLength()); + } + if (schemaAnnotation.maxLength() > 0 && schemaAnnotation.maxLength() != Integer.MAX_VALUE) { + fieldSchema.put("maxLength", schemaAnnotation.maxLength()); + } + if (schemaAnnotation.allowableValues().length > 0 && !schemaAnnotation.allowableValues()[0].isEmpty()) { + fieldSchema.put("enum", List.of(schemaAnnotation.allowableValues())); + } + if (!schemaAnnotation.format().isEmpty()) { + fieldSchema.put("format", schemaAnnotation.format()); + } + if (!schemaAnnotation.defaultValue().isEmpty()) { + fieldSchema.put("default", coerceDefaultValue(schemaAnnotation.defaultValue(), field.getType())); + } + if (!schemaAnnotation.example().isEmpty()) { + fieldSchema.put("examples", List.of(schemaAnnotation.example())); + } + if (schemaAnnotation.nullable()) { + Map nonNullSchema = new HashMap<>(fieldSchema); + fieldSchema = new HashMap<>(); + fieldSchema.put("anyOf", List.of( + nonNullSchema, + Map.of("type", "null") + )); + } } - if (validation != null && !validation.pattern().isEmpty()) { - fieldSchema.put("pattern", validation.pattern()); - } - if (validation != null && validation.minimum() != Long.MIN_VALUE) { - fieldSchema.put("minimum", validation.minimum()); - } - if (validation != null && validation.maximum() != Long.MAX_VALUE) { - fieldSchema.put("maximum", validation.maximum()); - } - if (validation != null && validation.minLength() >= 0) { - fieldSchema.put("minLength", validation.minLength()); - } - if (validation != null && validation.maxLength() >= 0) { - fieldSchema.put("maxLength", validation.maxLength()); - } - if (validation != null && validation.allowedValues().length > 0) { - fieldSchema.put("enum", List.of(validation.allowedValues())); - } - if (validation != null && !validation.format().isEmpty()) { - fieldSchema.put("format", validation.format()); - } - if (validation != null && !validation.defaultValue().isEmpty()) { - fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType())); - } - if (validation != null && validation.examples().length > 0) { - fieldSchema.put("examples", List.of(validation.examples())); - } - if (validation != null && validation.nullable()) { - Map nonNullSchema = new HashMap<>(fieldSchema); - fieldSchema = new HashMap<>(); - fieldSchema.put("anyOf", List.of( - nonNullSchema, - Map.of("type", "null") - )); - } - properties.put(field.getName(), fieldSchema); } @@ -120,15 +127,7 @@ public class JsonSchemaGenerator { schema.put("required", requiredList); } - McpAnyOf anyOfAnnotation = clazz.getAnnotation(McpAnyOf.class); - if (anyOfAnnotation != null && anyOfAnnotation.value().length > 0) { - List> anyOfList = new ArrayList<>(); - for (String fieldName : anyOfAnnotation.value()) { - anyOfList.add(Map.of("required", List.of(fieldName))); - - } - schema.put("anyOf", anyOfList); - } + // anyOf removed visiting.remove(clazz); return schema; 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 new file mode 100644 index 000000000..ea648ea72 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java @@ -0,0 +1,309 @@ +package io.shinhanlife.dap.lib.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Scanner; + +public class PodScaffolder { + + public static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================="); + System.out.println(" MCP Tool Pod Scaffolder (Java CLI) "); + System.out.println("=========================================\n"); + + 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-was-", "").replace("-", ""); + + String defaultAuthor = System.getProperty("user.name"); + String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + if (author.trim().isEmpty()) author = defaultAuthor; + String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + if (createDate.trim().isEmpty()) createDate = defaultDate; + + String result = scaffoldPod(moduleName, portStr, shortName, author, createDate); + System.out.println(result); + } + + private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) { + if (args.length > index) { + return args[index]; + } + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException { + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + Path modulePath = rootDir.resolve(Paths.get(moduleName)); + if (Files.exists(modulePath)) { + return "[오류] 이미 존재하는 모듈입니다: " + moduleName; + } + + StringBuilder log = new StringBuilder(); + log.append("[1/6] 모듈 디렉터리 생성 중...\n"); + Files.createDirectories(modulePath); + + log.append("[2/6] build.gradle 생성 중...\n"); + String buildGradle = """ + plugins { + id 'org.springframework.boot' + } + dependencies { + implementation project(':dap-was-lib') + } + dependencies { + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + } + """; + Files.writeString(modulePath.resolve("build.gradle"), buildGradle); + + log.append("[3/6] Dockerfile 생성 중...\n"); + String dockerfile = """ + FROM eclipse-temurin:21-jdk-alpine + WORKDIR /app + COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar + ENTRYPOINT ["java", "-jar", "app.jar"] + """.formatted(moduleName); + Files.writeString(modulePath.resolve("Dockerfile"), dockerfile); + + log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n"); + Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName); + Files.createDirectories(srcPath); + + String appClass = """ + package io.shinhanlife.dap.mcc.%s; + + import org.springframework.boot.SpringApplication; + import org.springframework.boot.autoconfigure.SpringBootApplication; + import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + import org.springframework.cache.annotation.EnableCaching; + + /** + * @package io.shinhanlife.dap.mcc.%s + * @className DapWas%sApplication + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"}) + @ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"}) + @EnableCaching + public class DapWas%sApplication { + public static void main(String[] args) { + SpringApplication.run(DapWas%sApplication.class, args); + } + } + """.formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName)); + Files.writeString(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass); + + Path resPath = modulePath.resolve("src/main/resources"); + Files.createDirectories(resPath); + String applicationYml = """ + server: + port: %s + spring: + application: + name: %s + profiles: + active: local + logging: + level: + org.apache.kafka: ERROR + mcp: + namespace: "" + security: + tenant-domains: + TESTER-DEV: ALL + """.formatted(portStr, moduleName); + Files.writeString(resPath.resolve("application.yml"), applicationYml); + + String applicationLocalYml = """ + # Local 환경 전용 설정 (H2 메모리 DB 등) + spring: + datasource: + url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1; + driverClassName: com.p6spy.engine.spy.P6SpyDriver + username: sa + password: password + h2: + console: + enabled: true + + eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + + mcp: + security: + tenant-domains: + mcp-client-1: CUSTOMER,COMMON + mcp-client-2: ALL + + axhub: + gateway: + url: http://localhost:8081 + tool: + url: ${AXHUB_TOOL_URL:http://localhost:${server.port}} + """; + Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml); + + String applicationDevYml = """ + # OCI 클라우드 환경 전용 설정 + server: + port: ${PORT:%s} + + axhub: + gateway: + url: https://axhubmcp.devjun.net + tool: + url: http://144.24.70.100:%s + + eims: + http: + url: http://localhost:${server.port}/api/gateway + tcp: + host: 127.0.0.1 + port: 8090 + timeout: 5000 + jsp: + form: + url: http://localhost:${server.port}/mock/jsp-form + json: + url: http://localhost:${server.port}/mock/jsp-json + mci: + url: http://localhost:${server.port}/api/mock/esb/api + mcistring: + url: http://localhost:${server.port}/api/mock/esb/string + + shinhan: + integration: + envrTypeCd: D + eai: + url: http://10.176.32.181 + internalMci: + url: http://10.176.32.173 + bancaMci: + url: http://10.176.32.117 + externalMci: + url: http://10.176.32.176 + """.formatted(portStr, portStr); + Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml); + + String logbackXml = """ + + + + + + ${LOG_PATTERN} + + + + logs/%s.log + + logs/%s-%%d{yyyy-MM-dd}.log + 30 + + + ${LOG_PATTERN} + + + + + + + + + """.formatted(moduleName, moduleName); + Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml); + + log.append("[5/6] settings.gradle 에 모듈 등록 중...\n"); + Path settingsPath = rootDir.resolve(Paths.get("settings.gradle")); + if (Files.exists(settingsPath)) { + String settings = Files.readString(settingsPath); + if (!settings.contains("include '" + moduleName + "'")) { + Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND); + } + } + + log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n"); + Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml")); + if (Files.exists(dockerComposePath)) { + String compose = Files.readString(dockerComposePath); + String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment + if (!compose.contains(" " + serviceName + ":")) { + String newService = """ + %s: + build: + context: . + dockerfile: %s/Dockerfile + ports: + - "%s:%s" + depends_on: + - redis + environment: + - TZ=Asia/Seoul + - SPRING_REDIS_HOST=redis + - SPRING_REDIS_PORT=6379 + - SPRING_DATA_REDIS_PORT=6379 + - AXHUB_GATEWAY_URL=http://gateway:8081 + - AXHUB_TOOL_URL=http://%s:%s + - GLOW_COMMUNICATION_MCI_HOST=http://mci-mock + - GLOW_COMMUNICATION_MCI_PORT=8080 + - GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock + - GLOW_COMMUNICATION_EXTMCI_PORT=8080 + - GLOW_COMMUNICATION_EAI_HOST=http://mci-mock + - GLOW_COMMUNICATION_EAI_PORT=8080 + """.formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr); + Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND); + } + } + + log.append("\n=========================================\n"); + log.append(" Pod Scaffolding Complete! \n"); + log.append("=========================================\n"); + log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n"); + log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n"); + log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n"); + return log.toString(); + } + + private static String capitalize(String str) { + if (str == null || str.isEmpty()) return str; + return str.substring(0, 1).toUpperCase() + str.substring(1); + } +} 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 new file mode 100644 index 000000000..3f4ed4fbe --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -0,0 +1,783 @@ +package io.shinhanlife.dap.lib.util; + + +import org.springframework.ai.mcp.annotation.McpToolParam; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Locale; +import java.util.Scanner; + +/** + * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스 + * + * [실행 방법] + * 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐) + * - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다. + * - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다. + * + * 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반) + * - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java + * - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]" + */ +/** + * @package io.shinhanlife.dap.lib.util + * @className ToolScaffolder + * @description AX HUB 시스템 처리 클래스 + * @author 0986406 + * @create 2026.09.01 + *
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
+ * ---------- -------- ---------------------------
+ * 2026.09.01  0986406    최초생성
+ * 
+ * 
+ */ +public class ToolScaffolder { + + private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc"; + private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc"; + + public static void main(String[] args) throws IOException { + Scanner scanner = new Scanner(System.in); + + System.out.println("========================================="); + System.out.println(" MCP Tool Scaffolder (Java CLI) "); + System.out.println("=========================================\n"); + + String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: "); + String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): "); + String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): "); + String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): "); + if (group.isEmpty()) group = "COMMON"; + String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): "); + if (routingType.trim().isEmpty()) { + routingType = "HTTP"; + } + String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-was-oth): "); + if (moduleName.trim().isEmpty()) { + moduleName = "dap-was-oth"; + } + + String defaultAuthor = System.getProperty("user.name"); + String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); + + String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + if (author.trim().isEmpty()) author = defaultAuthor; + String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + if (createDate.trim().isEmpty()) createDate = defaultDate; + + String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): "); + boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim()); + + String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, useSchemaResource); + System.out.println(result); + } + + private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) { + if (args.length > index) { + return args[index]; + } + System.out.print(prompt); + return scanner.nextLine().trim(); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException { + return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, register, clientSystemCode, false); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, boolean useSchemaResource) throws IOException { + baseName = toPascalCase(baseName); + String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); + + Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); + Path usecaseImplDir = usecaseDir.resolve("impl"); + Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto")); + + Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy")); + Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter")); + + // schema resource 파일 경로 (useSchemaResource=true 일 때만 생성) + String schemaBaseName = toKebabCase(baseName); + String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json"; + String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json"; + Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mcp/schema")); + String inputSchemaClasspath = "classpath:mcp/schema/" + inputSchemaFileName; + String outputSchemaClasspath = "classpath:mcp/schema/" + outputSchemaFileName; + + String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase(); + boolean isMci = "MCI".equalsIgnoreCase(routingType); + String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase(); + String clientPkgSuffix = ""; + String clientPrefixCap = ""; + Path mciClientDir = null; + + if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) { + String clientPrefix = clientSystemCode.toLowerCase(); + clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4); + clientPrefixCap = toPascalCase(clientSystemCode); + mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4); + mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath)); + } + + Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io")); + + Files.createDirectories(usecaseDir); + Files.createDirectories(usecaseImplDir); + Files.createDirectories(dtoDir); + if (isMci) { + Files.createDirectories(mciIoDir); + if (mciClientDir != null) { + Files.createDirectories(mciClientDir); + } + } else { + Files.createDirectories(legacyDtoDir); + } + Files.createDirectories(converterDir); + + StringBuilder log = new StringBuilder(); + + // Generate Request DTO + String reqContent = """ + package %s.dto; + + import com.fasterxml.jackson.annotation.JsonInclude; + import lombok.Data; + + /** + * @package %s.dto + * @className %sRequest + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %sRequest { + @McpToolParam(description = "수신자 전화번호", required = true) + -(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"}) + private String phoneNumber; + + @McpToolParam(description = "전송할 메시지 내용", required = true) + private String message; + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); + + // Generate Response DTO + String resContent = """ + package %s.dto; + + import com.fasterxml.jackson.annotation.JsonInclude; + import lombok.Data; + + /** + * @package %s.dto + * @className %sResponse + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             * 
+             * 
+ */ + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %sResponse { + private String resultCode; + + private String resultMessage; + + // TODO: Add response fields here. Do not include PII in the Tool response. + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent); + + String toolName = toToolName(moduleName, group, baseName); + + String toolHintLine; + if (useSchemaResource) { + toolHintLine = " @ToolHint(register = %s,\n" + + " inputSchemaResource = \"%s\",\n" + + " outputSchemaResource = \"%s\")".formatted(register, inputSchemaClasspath, outputSchemaClasspath); + } else { + toolHintLine = " @ToolHint(register = %s)".formatted(register); + } + + String serviceInterfaceContent = """ + package %s.usecase; + + import org.springframework.ai.mcp.annotation.McpTool; + import io.shinhanlife.dap.lib.annotation.ToolHint; + import %s.dto.%sRequest; + import %s.dto.%sResponse; + + /** + * @package %s.usecase + * @className %sUseCase + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+             * ---------- 개정이력 ----------
+             * 수정일      수정자    수정내용
+             * ---------- -------- ---------------------------
+             * %s  %s    최초생성
+             *
+             * 
+ */ + public interface %sUseCase { + + @McpTool(name = "%s", title = "%s", description = "%s") + %s + %sResponse execute(%sRequest req); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, author, createDate, createDate, author, + baseName, + toolName, description, description, + toolHintLine, + baseName, baseName + ); + + Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent); + + String serviceImplContent; + + if (isMci) { + serviceImplContent = """ + package %s.usecase.impl; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + import java.util.Map; + import %s.converter.%sConverter; + import %s.%s.io.%s_I; + %s + + /** + * @package %s.usecase.impl + * @className %sUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl implements %sUseCase { + + %s + private final %sConverter converter; + + @Override + public Object execute(%sRequest req) { + log.info("[MCI Tool] {} 요청 수신.", "%s"); + try { + // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) + %s_I mciReq = converter.toLegacyRequest(req); + + Transfer resTransfer = mci.callTo( + "%s", + null, + mciReq, + Object.class + ); + return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS"); + } catch (Exception e) { + log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error"); + } + } + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + (clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"), + bizPackage, + baseName, + author, + createDate, + createDate, author, + baseName, + baseName, + (clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"), + baseName, + baseName, + toolName, + interfaceId, + interfaceId + ); + } else { + serviceImplContent = """ + package %s.usecase.impl; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.usecase.%sUseCase; + import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; + import %s.converter.%sConverter; + import org.springframework.stereotype.Service; + import lombok.RequiredArgsConstructor; + import lombok.extern.slf4j.Slf4j; + + /** + * @package %s.usecase.impl + * @className %sUseCaseImpl + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Slf4j + @Service + @RequiredArgsConstructor + public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase { + + private final %sConverter converter; + + @Override + public Object execute(%sRequest req) { + // %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); + return executeLegacy("%s", "%s", req); // Or pass legacyRequest + } + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + author, + createDate, + createDate, author, + baseName, baseName, + baseName, + baseName, + baseName, + routingType, interfaceId + ); + } + + Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent); + + if (isMci) { + String mciReqContent = """ + package %s.%s.io; + + import lombok.Data; + + /** + * @package %s.%s.io + * @className %s_I + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %s_I { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; + } + """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent); + + String mciResContent = """ + package %s.%s.io; + + import lombok.Data; + + /** + * @package %s.%s.io + * @className %s_O + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %s_O { + // TODO: Add response fields here + } + """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent); + + String converterContent = """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.%s.io.%s_I; + import %s.%s.io.%s_O; + import org.mapstruct.Mapper; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.converter + * @className %sConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + %s_I toLegacyRequest(%sRequest req); + + @Mapping(source = "phone", target = "phoneNumber") + @Mapping(source = "content", target = "message") + %sRequest toRequest(%s_I mciReq); + + // %sResponse toResponse(%s_O mciRes); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, + bizPackage, baseName, author, createDate, createDate, author, + baseName, interfaceId, baseName, + baseName, interfaceId, + baseName, interfaceId + ); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); + log.append("=========================================\n"); + log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n"); + log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n"); + log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n"); + log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n"); + log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n"); + log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n"); + log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); + + if (!clientPrefixCap.isEmpty()) { + String mciClientContent = """ + package %s.%s; + + import org.springframework.stereotype.Component; + import lombok.RequiredArgsConstructor; + import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent; + import io.shinhanlife.glow.communication.dto.Transfer; + + /** + * @package %s.%s + * @className Mci%sClient + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                     * ---------- 개정이력 ----------
+                     * 수정일      수정자    수정내용
+                     * ---------- -------- ---------------------------
+                     * %s  %s    최초생성
+                     * 
+                     * 
+ */ + @Component + @RequiredArgsConstructor + public class Mci%sClient { + private final AxhubMciComponent mci; + + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + return mci.callTo(interfaceId, dummy, mciReq, resType); + } + } + """.formatted( + BASE_PACKAGE, mciGroupPath.replace("/", "."), + BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap + ); + Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent); + log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n"); + } + + } else { + String legacyReqContent = """ + package %s.legacy; + + import lombok.Data; + + /** + * @package %s.legacy + * @className %sLegacyRequest + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyRequest { + /** + * EAI 시스템이 요구하는 수신자 번호 파라미터명 + */ + private String phone; + + /** + * EAI 시스템이 요구하는 메시지 내용 파라미터명 + */ + private String content; + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent); + + String legacyResContent = """ + package %s.legacy; + + import lombok.Data; + + /** + * @package %s.legacy + * @className %sLegacyResponse + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Data + public class %sLegacyResponse { + // TODO: Add legacy response fields here + } + """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent); + + String converterContent = """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.legacy.%sLegacyRequest; + import %s.legacy.%sLegacyResponse; + import org.mapstruct.Mapper; + import org.mapstruct.Mapping; + import org.mapstruct.factory.Mappers; + + /** + * @package %s.converter + * @className %sConverter + * @description AX HUB 시스템 처리 클래스 + * @author %s + * @create %s + *
+                 * ---------- 개정이력 ----------
+                 * 수정일      수정자    수정내용
+                 * ---------- -------- ---------------------------
+                 * %s  %s    최초생성
+                 * 
+                 * 
+ */ + @Mapper(componentModel = "spring") + public interface %sConverter { + + @Mapping(source = "phoneNumber", target = "phone") + @Mapping(source = "message", target = "content") + %sLegacyRequest toLegacyRequest(%sRequest req); + + @Mapping(source = "phone", target = "phoneNumber") + @Mapping(source = "content", target = "message") + %sRequest toRequest(%sLegacyRequest legacyRequest); + + // %sResponse toResponse(%sLegacyResponse legacyResponse); + } + """.formatted( + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, author, createDate, createDate, author, + baseName, baseName, baseName, baseName, baseName, baseName, baseName + ); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); + log.append("=========================================\n"); + log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n"); + log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n"); + log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n"); + log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n"); + log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n"); + log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n"); + log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); + } + // schema resource 파일 생성 (useSchemaResource=true 일 때) + if (useSchemaResource) { + Files.createDirectories(schemaDir); + String inputSchema = """ + { + "type": "object", + "additionalProperties": false, + "properties": { + "TODO_FIELD": { + "type": "string", + "description": "TODO: 파라미터 설명을 입력하세요." + } + }, + "required": [] + } + """; + String outputSchema = """ + { + "type": "object", + "additionalProperties": false, + "properties": { + "status": { + "type": "string", + "description": "처리 결과 상태 (SUCCESS / FAILURE)", + "enum": ["SUCCESS", "FAILURE"] + }, + "message": { + "type": "string", + "description": "처리 결과 메시지" + } + }, + "required": ["status"] + } + """; + Files.writeString(schemaDir.resolve(inputSchemaFileName), inputSchema); + Files.writeString(schemaDir.resolve(outputSchemaFileName), outputSchema); + log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n"); + log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n"); + } + + log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); + + return log.toString(); + } + + private static String toKebabCase(String pascalCase) { + if (pascalCase == null || pascalCase.isEmpty()) return pascalCase; + return pascalCase + .replaceAll("([a-z0-9])([A-Z])", "$1-$2") + .toLowerCase(Locale.ROOT); + } + + private static String toToolName(String moduleName, String group, String baseName) { + 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) + .replaceAll("[^a-z0-9]+", " ") + .trim(); + String[] words = normalizedName.split("\\s+"); + String service = words[0]; + String action = words.length == 1 ? "execute" : words[words.length - 1]; + return "%s.%s.%s.%s".formatted( + pod.toLowerCase(Locale.ROOT), + group.toLowerCase(Locale.ROOT), + service, + action); + } + + private static String toPascalCase(String str) { + if (str == null || str.isEmpty()) { + return str; + } + StringBuilder result = new StringBuilder(); + boolean capitalizeNext = true; + for (char c : str.toCharArray()) { + if (c == '_' || c == '-' || c == ' ') { + capitalizeNext = true; + } else if (capitalizeNext) { + result.append(Character.toUpperCase(c)); + capitalizeNext = false; + } else { + result.append(c); + } + } + if (result.length() > 0) { + result.setCharAt(0, Character.toUpperCase(result.charAt(0))); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java index 12f98a106..ebc4ed136 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java @@ -1,28 +1,14 @@ package io.shinhanlife.dap.lib.util; -/** - * @package io.shinhanlife.dap.lib.util - * @className ToolSchemaResolver - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import io.shinhanlife.dap.lib.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream; +// removed McpOutputSchema +import java.io.InputStream; import java.util.Map; import org.springframework.core.io.ClassPathResource; +import io.shinhanlife.dap.lib.annotation.ToolHint; + /** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */ public class ToolSchemaResolver { @@ -32,13 +18,9 @@ public class ToolSchemaResolver { this.objectMapper = objectMapper; } - public Map resolve(McpFunction function, Class requestType) { - if (function != null && !function.inputSchemaResource().isBlank()) { - return loadResource(function.inputSchemaResource()); - } - if (function != null && !function.inputSchema().isBlank() - && !"{}".equals(function.inputSchema().trim())) { - return parse(function.inputSchema(), "McpFunction.inputSchema"); + public Map resolve(org.springframework.ai.mcp.annotation.McpTool function, ToolHint hint, Class requestType) { + if (hint != null && !hint.inputSchemaResource().isBlank()) { + return loadResource(hint.inputSchemaResource()); } return JsonSchemaGenerator.generateSchema(requestType); } @@ -46,25 +28,36 @@ public class ToolSchemaResolver { /** * Resolves an explicitly declared response schema. * Response schemas are opt-in so existing tools keep their current response behavior. + * ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고, + * 없으면 responseType DTO를 분석하여 자동 생성합니다. */ - public Map resolveOutput(McpFunction function, Class responseType) { - if (function != null && !function.outputSchemaResource().isBlank()) { - return loadResource(function.outputSchemaResource()); + public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class responseType, ToolHint hint) { + if (hint != null && !hint.outputSchemaResource().isBlank()) { + return loadResource(hint.outputSchemaResource()); } - if (function != null && !function.outputSchema().isBlank() - && !"{}".equals(function.outputSchema().trim())) { - return parse(function.outputSchema(), "McpFunction.outputSchema"); + return resolveOutput(function, responseType); + } + + /** + * Resolves an explicitly declared response schema. + * Response schemas are opt-in so existing tools keep their current response behavior. + */ + public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class responseType) { + // Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵 + if (responseType == null + || responseType == Object.class + || Map.class.isAssignableFrom(responseType) + || responseType == Void.class + || responseType == void.class) { + return Map.of(); } - if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) { - return JsonSchemaGenerator.generateSchema(responseType); - } - return Map.of(); + return JsonSchemaGenerator.generateSchema(responseType); } /** * Retained for callers that use only explicit output schemas. */ - public Map resolveOutput(McpFunction function) { + public Map resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) { return resolveOutput(function, null); } 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 new file mode 100644 index 000000000..f03845247 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolSourceUpdater.java @@ -0,0 +1,140 @@ +package io.shinhanlife.dap.lib.util; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; + +/** Updates MCP SDK and project-owned metadata in a generated tool source file. */ +public final class ToolSourceUpdater { + + private ToolSourceUpdater() { + } + + public static void updateToolSource(String toolName, String categoryKey, String description, + boolean register, Boolean requiresApproval) throws IOException { + String configuredSourceDirectory = System.getenv("AXHUB_SOURCE_DIR"); + Path rootDirectory = configuredSourceDirectory == null || configuredSourceDirectory.isBlank() + ? Paths.get(".") : Paths.get(configuredSourceDirectory); + updateToolSource(rootDirectory, toolName, categoryKey, description, register, requiresApproval); + } + + static void updateToolSource(Path rootDirectory, String toolName, String categoryKey, String description, + boolean register, Boolean requiresApproval) throws IOException { + Path targetFile = findToolSource(rootDirectory, toolName); + if (targetFile == null) { + throw new IllegalArgumentException("Tool source not found: " + toolName); + } + + String content = Files.readString(targetFile); + content = updateMcpTool(content, toolName, description); + content = updateToolHint(content, categoryKey, register, requiresApproval); + Files.writeString(targetFile, content); + } + + private static Path findToolSource(Path rootDirectory, String toolName) throws IOException { + try (var paths = Files.walk(rootDirectory)) { + return paths.filter(Files::isRegularFile) + .filter(path -> path.getFileName().toString().endsWith("UseCase.java")) + .filter(path -> path.toString().contains("dap-was-") || path.toString().contains("axhub-tool-")) + .sorted(Comparator.naturalOrder()) + .filter(path -> containsMcpTool(path, toolName)) + .findFirst() + .orElse(null); + } + } + + private static boolean containsMcpTool(Path path, String toolName) { + try { + return annotationArguments(Files.readString(path), "McpTool", toolName) != null; + } catch (IOException exception) { + throw new IllegalStateException("Failed to read tool source: " + path, exception); + } + } + + private static String updateMcpTool(String content, String toolName, String description) { + AnnotationRange range = annotationArguments(content, "McpTool", toolName); + if (range == null) { + throw new IllegalArgumentException("McpTool declaration not found: " + toolName); + } + return description == null ? content : replaceAttribute(content, range, "description", quote(description)); + } + + private static String updateToolHint(String content, String categoryKey, boolean register, Boolean requiresApproval) { + AnnotationRange range = annotationArguments(content, "ToolHint", null); + if (range == null) { + throw new IllegalArgumentException("ToolHint declaration not found next to McpTool"); + } + String updated = replaceAttribute(content, range, "register", Boolean.toString(register)); + range = annotationArguments(updated, "ToolHint", null); + if (requiresApproval != null) { + updated = replaceAttribute(updated, range, "requiresApproval", Boolean.toString(requiresApproval)); + range = annotationArguments(updated, "ToolHint", null); + } + if (categoryKey != null && !categoryKey.isBlank()) { + updated = replaceAttribute(updated, range, "categoryKey", quote(categoryKey)); + } + return updated; + } + + private static String replaceAttribute(String content, AnnotationRange range, String attribute, String value) { + String arguments = content.substring(range.argumentsStart(), range.argumentsEnd()); + String pattern = "\\b" + attribute + "\\s*=\\s*(?:true|false|\\\"(?:\\\\.|[^\\\"\\\\])*\\\")"; + String replacement = arguments.replaceFirst(pattern, attribute + " = " + value); + if (replacement.equals(arguments)) { + replacement = arguments.isBlank() ? attribute + " = " + value : arguments + ", " + attribute + " = " + value; + } + return content.substring(0, range.argumentsStart()) + replacement + content.substring(range.argumentsEnd()); + } + + private static String quote(String value) { + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\""; + } + + private static AnnotationRange annotationArguments(String content, String annotationName, String toolName) { + int offset = content.indexOf("@" + annotationName); + while (offset >= 0) { + int openingParenthesis = content.indexOf('(', offset); + int closingParenthesis = findAnnotationEnd(content, openingParenthesis); + if (openingParenthesis < 0 || closingParenthesis < 0) { + return null; + } + AnnotationRange range = new AnnotationRange(openingParenthesis + 1, closingParenthesis); + if (toolName == null || content.substring(range.argumentsStart(), range.argumentsEnd()) + .matches("(?s).*\\bname\\s*=\\s*\\\"" + java.util.regex.Pattern.quote(toolName) + "\\\".*")) { + return range; + } + offset = content.indexOf("@" + annotationName, closingParenthesis + 1); + } + return null; + } + + private static int findAnnotationEnd(String content, int openingParenthesis) { + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int index = openingParenthesis; index < content.length(); index++) { + char character = content.charAt(index); + if (inString) { + if (escaped) { + escaped = false; + } else if (character == '\\') { + escaped = true; + } else if (character == '\"') { + inString = false; + } + } else if (character == '\"') { + inString = true; + } else if (character == '(') { + depth++; + } else if (character == ')' && --depth == 0) { + return index; + } + } + return -1; + } + + private record AnnotationRange(int argumentsStart, int argumentsEnd) { + } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java index e495f517d..414843e38 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/McpToolNameValidationRunner.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.validation; -/** - * @package io.shinhanlife.dap.lib.validation - * @className McpToolNameValidationRunner - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.nio.file.Path; /** Gradle entry point for validating unique MCP Tool names before packaging. */ 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 4a64fd8de..89aea24d7 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 @@ -1,21 +1,5 @@ package io.shinhanlife.dap.lib.validation; -/** - * @package io.shinhanlife.dap.lib.validation - * @className McpToolNameValidator - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; @@ -31,7 +15,7 @@ import java.util.regex.Pattern; /** * @package io.shinhanlife.dap.lib.validation * @className McpToolNameValidator - * @description Validates unique MCP function names across tool modules + * @description Validates unique MCP SDK tool names across tool modules * @author 0986406 * @create 2026.07.27 *
@@ -104,7 +88,7 @@ public final class McpToolNameValidator {
             throw new UncheckedIOException("Failed to read " + source, exception);
         }
 
-        int annotationOffset = content.indexOf("@McpFunction");
+        int annotationOffset = content.indexOf("@McpTool");
         while (annotationOffset >= 0) {
             int openingParenthesis = content.indexOf('(', annotationOffset);
             int closingParenthesis = findAnnotationEnd(content, openingParenthesis);
@@ -119,7 +103,7 @@ public final class McpToolNameValidator {
                 declarationsByName.computeIfAbsent(toolName, ignored -> new ArrayList<>())
                         .add(new ToolDeclaration(moduleName, source, line));
             }
-            annotationOffset = content.indexOf("@McpFunction", closingParenthesis + 1);
+            annotationOffset = content.indexOf("@McpTool", closingParenthesis + 1);
         }
     }
 
diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java
index bdea2810c..c138ab077 100644
--- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java
+++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java
@@ -1,21 +1,5 @@
 package io.shinhanlife.dap.lib.validation;
 
-/**
- * @package io.shinhanlife.dap.lib.validation
- * @className ToolArgumentSchemaValidator
- * @description AX HUB 시스템 처리 클래스
- * @author 0986406
- * @create 2026.09.01
- * 
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.Error; import com.networknt.schema.InputFormat; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java index 9ab099034..44e9f7d9e 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/dto/ToolMetadata.java @@ -2,14 +2,23 @@ package io.shinhanlife.dap.mcc.dto; import lombok.Getter; import lombok.Setter; -import lombok.Builder; -import lombok.NoArgsConstructor; + import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.Map; - +import java.util.Set; +import java.util.List; +import java.util.HashSet; +import io.shinhanlife.dap.lib.dto.OperationType; /** - * @package io.shinhanlife.dap.mcc.dto + * Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스 + * Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다. + */ +/** + * @package io.shinhanlife.dap.mcg.dto * @className ToolMetadata * @description AX HUB 시스템 처리 클래스 * @author 0986406 @@ -22,17 +31,16 @@ import java.util.Map; * *
*/ -@Getter -@Setter @Builder @NoArgsConstructor @AllArgsConstructor +@Getter +@Setter public class ToolMetadata { + // 1. Tool 기본 정보 private String uid; // UUID 형식의 고유 식별자 private String semver; // 버전 (예: 1.0.0) - private Long timeoutMillis; - private Boolean enabled; private String displayName; // 사람이 읽는 라벨 (1-128자) private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool) private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능) @@ -45,18 +53,26 @@ public class ToolMetadata { // 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식) private String categoryKey; + + // 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info) private String endpoint; + + // 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082) private String podUrl; - private String integrationType; - private String mciServiceId; - + // 2-4. 가시성 여부 @Builder.Default private Boolean visible = true; + + // 활성화 여부 + @Builder.Default + private Boolean enabled = true; + // 2-5. Redis 등록 여부 (UI 표출용) @Builder.Default private Boolean isRegistered = true; + // 2-6. HITL 승인 필요 여부 @Builder.Default private Boolean requiresApproval = false; @@ -73,4 +89,45 @@ public class ToolMetadata { private Boolean openWorldHint = false; + + // 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI) + private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI" + + // 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용) + private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992) + + // 5. 인프라 상태 정보 (DIRECT 연동 시 사용) + private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프 + + // 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반) + private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%) + private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수 + private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수 + + // 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입) + @Builder.Default + private OperationType operationType = OperationType.READ; + + @Builder.Default + private Boolean retryEnabled = true; + + @Builder.Default + private Integer circuitBreakerFailureThreshold = 0; + + @Builder.Default + private Long circuitBreakerOpenMillis = 0L; + + @Builder.Default + private Long timeoutMillis = 0L; + + // --- Guardrail 호환성을 위한 메서드 추가 --- + public Set allowedArguments() { + if (parametersSchema == null || !parametersSchema.containsKey("properties")) return Set.of(); + return ((Map) parametersSchema.get("properties")).keySet(); + } + + public Set requiredArguments() { + if (parametersSchema == null || !parametersSchema.containsKey("required")) return Set.of(); + return new HashSet<>((List) parametersSchema.get("required")); + } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java index 770415bbf..c3f42a2b7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java @@ -4,25 +4,24 @@ package io.shinhanlife.dap.mcc.presentation; /** * @package io.shinhanlife.dap.mcc.presentation * @className BusinessToolController - * @description AX HUB ??戮?츩??嶺뚳퐣瑗???????? + * @description AX HUB 시스템 처리 클래스 * @author 0986406 * @create 2026.09.01 *
- * ---------- ?띠룇裕??????----------
- * ??瑜곸젧??     ??瑜곸젧??   ??瑜곸젧??怨몃뮔
+ * ---------- 개정이력 ----------
+ * 수정일      수정자    수정내용
  * ---------- -------- ---------------------------
- * 2026.09.01  0986406    嶺뚣끉裕???諛댁뎽
+ * 2026.09.01  0986406    최초생성
  * 
  * 
*/ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.Error; +import org.springframework.ai.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; -import io.shinhanlife.dap.lib.annotation.McpFunction; -import io.shinhanlife.dap.lib.annotation.McpTool; import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.mcc.dto.ToolMetadata; -import io.shinhanlife.dap.lib.mcp.LocalToolScanner; +import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import java.lang.reflect.Method; import java.util.ArrayList; @@ -55,17 +54,17 @@ public class BusinessToolController { private final ApplicationContext applicationContext; private final ObjectMapper objectMapper; private final McpProperties mcpProperties; - private final LocalToolScanner localToolScanner; + private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender; private final ToolArgumentSchemaValidator toolArgumentSchemaValidator; private final ToolSchemaResolver toolSchemaResolver; - // ???? ?브퀗?????β돦裕뉛쭚?Tool 嶺뚮ㅄ維뽨빳???븐뼔援????? + // 내부 조회용 로컬 Tool 목록 엔드포인트 @GetMapping("/mcp/api/v1/tools/local") public List getLocalTools() { - return localToolScanner.getAllScannedTools(); + return toolRegistryHeartbeatSender.getAllScannedTools(); } - // ??戮?빢 REST ?リ옇?↑€????됱쓤 ??源녿뮡????븐뼔援????? + // 순수 REST 기반 동적 라우팅 엔드포인트 @PostMapping("/mcp/{name}") public ResponseEntity executeDynamicTool( @PathVariable("name") String functionName, @@ -78,25 +77,25 @@ public class BusinessToolController { String finalRequestId = headerRequestId; log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId); - log.info(" [Tool] ???됱쓤 ?????덈뺄 ??븐슙????琉용뼁 (??貫?양춯?: {}", functionName); + log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName); if (arguments != null) { try { - log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", objectMapper.writeValueAsString(arguments)); + log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments)); } catch (Exception e) { - log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", arguments); + log.info(" [Tool] 호출 파라미터: {}", arguments); } } Object targetBean = null; Method targetMethod = null; - McpFunction targetFunctionAnnotation = null; + McpTool targetFunctionAnnotation = null; - // McpTool ???????逾???リ옇?↑€??브퀗???뱀쾸? ?熬곣뫁夷????쒖굣?節놁뿉??熬곣뫁逾???????깅さ亦껋깢??? ?熬곣뫕??????????щ뤂????@McpFunction??嶺뚢돦堉????덈펲. + // McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpTool을 찾습니다. Map allBeans = applicationContext.getBeansOfType(Object.class); outerLoop: for (Object bean : allBeans.values()) { Class targetClass = AopUtils.getTargetClass(bean); for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) { - McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class); + McpTool mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpTool.class); if (mcpFunc != null) { String baseName = mcpFunc.name(); String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() @@ -122,7 +121,7 @@ public class BusinessToolController { for (Object bean : allBeans.values()) { Class targetCls = AopUtils.getTargetClass(bean); for (Method m : targetCls.getDeclaredMethods()) { - McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class); + McpTool func = AnnotationUtils.findAnnotation(m, McpTool.class); if (func != null) { String baseName = func.name(); String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty() @@ -131,28 +130,29 @@ public class BusinessToolController { } } } - log.error("[Tool] ???덈뺄????貫??Method)??嶺뚢돦堉??????怨룸????덈펲: {}. ?熬곣뫗?????노뼌????嶺뚮∥?꾥땻??嶺뚮ㅄ維뽨빳? {}", functionName, availableFunctions); + log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions); Map errorDetails = new HashMap<>(); errorDetails.put("status", "404"); Map errorBody = new HashMap<>(); errorBody.put("code", "TOOL_NOT_FOUND"); - errorBody.put("message", "???덈뺄????貫???嶺뚢돦堉??????怨룸????덈펲: " + functionName); + errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName); errorBody.put("details", errorDetails); if (finalRequestId != null) errorBody.put("request_id", finalRequestId); return ResponseEntity.status(404).body(errorBody); } - // (?リ옇???嶺뚢뼰維€???β돦裕뉐퐲???蹂ㅽ깴?? + // (기존 차단 로직 제거됨) - // 2. ???逾ф쾬?롮구????ル쪇????롪틵?嶺?(JSON Schema) + // 2. 파라미터 유효성 검증 (JSON Schema) if (targetMethod.getParameterCount() > 0) { Class paramType = targetMethod.getParameterTypes()[0]; if (!Map.class.isAssignableFrom(paramType)) { try { - Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType); + io.shinhanlife.dap.lib.annotation.ToolHint hint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class); + Map inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, hint, paramType); List errors = toolArgumentSchemaValidator.validate(inputSchema, arguments); if (!errors.isEmpty()) { - log.error("[Tool] ???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮: {}", errors); + log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors); List errorMessages = new ArrayList<>(); for (Error validationError : errors) { errorMessages.add(validationError.getMessage()); @@ -161,28 +161,28 @@ public class BusinessToolController { errorDetails.put("status", "422"); Map errorBody = new HashMap<>(); errorBody.put("code", "INVALID_PARAM"); - errorBody.put("message", "???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮"); + errorBody.put("message", "파라미터 유효성 검증 실패"); errorBody.put("details", errorDetails); if (finalRequestId != null) errorBody.put("request_id", finalRequestId); return ResponseEntity.status(422).body(errorBody); } } catch (Exception e) { - log.error("[Tool] ???꾪뀞嶺??롪틵?嶺?繞????댁쾼 ?꾩룇裕뉑틦? {}", e.getMessage()); + log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage()); } } } - log.info("[Tool] ?洹먮뿫???源끒€?嶺뚯쉳??????덈뺄 -> Method: {}", targetMethod.getName()); + log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName()); try { - // 3. DTO ???逾ф쾬?롮구?????吏?嶺뚮씞?뗩뇡?(Map -> DTO) + // 3. DTO 파라미터 자동 매핑 (Map -> DTO) Object invokeArgument = arguments; if (targetMethod.getParameterCount() > 0 && arguments != null) { Class paramType = targetMethod.getParameterTypes()[0]; if (!Map.class.isAssignableFrom(paramType)) { invokeArgument = objectMapper.convertValue(arguments, paramType); - log.info("[Tool] DTO ???吏?嶺뚮씞?뗩뇡??繹먭퍓沅? {}", paramType.getSimpleName()); + log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName()); } } @@ -194,7 +194,8 @@ public class BusinessToolController { methodResult = targetMethod.invoke(targetBean, invokeArgument); } - Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType()); + io.shinhanlife.dap.lib.annotation.ToolHint outputHint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class); + Map outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType(), outputHint); if (!outputSchema.isEmpty()) { List outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult); if (!outputErrors.isEmpty()) { @@ -212,11 +213,11 @@ public class BusinessToolController { long elapsed = System.currentTimeMillis() - startTime; - // 5. ?롪퍒????꾩룇瑗??(??戮?빢 REST ??얜Ŧ堉? + // 5. 결과 반환 (순수 REST 응답) try { - log.info("[Tool Execution] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); + log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult)); } catch (Exception e) { - log.info("[Tool Execution] Output Schema Result: {}", methodResult); + log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult); } log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId); @@ -228,7 +229,7 @@ public class BusinessToolController { return responseBuilder.body(methodResult); } catch (Exception e) { - log.error("[Tool] ?洹먮뿫???源끒€????덈뺄 繞????깅뇶 ?꾩룇裕뉑틦? {}", e.getMessage()); + log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage()); Map errorDetails = new HashMap<>(); errorDetails.put("status", "500"); Map errorBody = new HashMap<>(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java index 3fed10f27..a8eeea7fc 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolManifestController.java @@ -1,21 +1,5 @@ package io.shinhanlife.dap.mcc.presentation; -/** - * @package io.shinhanlife.dap.mcc.presentation - * @className ToolManifestController - * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 媛쒖젙?대젰 ----------
- * ?섏젙??     ?섏젙??   ?섏젙?댁슜
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    理쒖큹?앹꽦
- * 
- * 
- */ - - import io.shinhanlife.dap.lib.manifest.ToolManifestResponse; import io.shinhanlife.dap.lib.manifest.ToolManifestService; import org.springframework.http.HttpHeaders; @@ -45,4 +29,4 @@ public class ToolManifestController { } return ResponseEntity.ok().eTag(eTag).body(manifest); } -} +} \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java index 24637daac..9463acf2b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/annotation/GlowTrgmField.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.annotation; -/** - * @package io.shinhanlife.glow.communication.annotation - * @className GlowTrgmField - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java index cc24e3226..7c991d52f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/CommonHeader.java @@ -1,20 +1,4 @@ package io.shinhanlife.glow.communication.dto; - -/** - * @package io.shinhanlife.glow.communication.dto - * @className CommonHeader - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - import lombok.Data; @Data public class CommonHeader { diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java index 968efe38e..7df3c0a7f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/HeaderDefaults.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.dto; -/** - * @package io.shinhanlife.glow.communication.dto - * @className HeaderDefaults - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - public enum HeaderDefaults { ITRF_ID, RCV_SVC_ID, STR_YMD, ACNT_OGNZ_NO, PSMR_ASRT_CD, SBSN_RULP_ASRT_CD, BSDU_CD, BSQU_CD, INDV_CTIN_ROLE_CD, SCRN_ID, OGNZ_ASRT_CD, OGNZ_LEVE_CD, TGRM_CREA_CHNN_TYPE_CD, ENVR_TYPE_CD diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java index 1cdcf82c0..31383d775 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/dto/Transfer.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.dto; -/** - * @package io.shinhanlife.glow.communication.dto - * @className Transfer - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java index 1cc14890b..464cc52e3 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/exception/ItrfException.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.exception; -/** - * @package io.shinhanlife.glow.communication.exception - * @className ItrfException - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - public class ItrfException extends Exception { public ItrfException(String msg) { super(msg); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java index 358469627..99bc00be7 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/eai/component/GlowEaiComponent.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.module.eai.component; -/** - * @package io.shinhanlife.glow.communication.module.eai.component - * @className GlowEaiComponent - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.shinhanlife.glow.communication.dto.Transfer; import org.springframework.stereotype.Component; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java index 44bc5d315..ecbe4a963 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/module/mci/component/GlowMciComponent.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.communication.module.mci.component; -/** - * @package io.shinhanlife.glow.communication.module.mci.component - * @className GlowMciComponent - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.shinhanlife.glow.communication.dto.Transfer; import org.springframework.stereotype.Component; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java index d0791306f..fbb154cd6 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/communication/util/CommonHeaderFactory.java @@ -1,20 +1,4 @@ package io.shinhanlife.glow.communication.util; - -/** - * @package io.shinhanlife.glow.communication.util - * @className CommonHeaderFactory - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - import io.shinhanlife.glow.communication.dto.CommonHeader; import io.shinhanlife.glow.communication.dto.HeaderDefaults; import java.util.Map; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java index e33bcf45b..5216ae1e8 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/util/GlowMciParser.java @@ -1,21 +1,5 @@ package io.shinhanlife.glow.util; -/** - * @package io.shinhanlife.glow.util - * @className GlowMciParser - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ - - import io.shinhanlife.glow.GlowMciFieldInfo; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.Field; diff --git a/dap-was-lib/src/main/resources/glow/application-glow.yml b/dap-was-lib/src/main/resources/glow/application-glow.yml index 97a7d1cc5..508f46c3f 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow.yml @@ -13,7 +13,7 @@ logging: pattern: console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n" -# 2. Redis 공통 설정 (WAS 연동 및 캐싱) +# 2. Redis 공통 설정 (Gateway 연동 및 캐싱) spring: data: redis: diff --git a/dap-was-lib/src/main/resources/static/tool-test-console.html b/dap-was-lib/src/main/resources/static/tool-test-console.html index c6574783c..951eda99c 100644 --- a/dap-was-lib/src/main/resources/static/tool-test-console.html +++ b/dap-was-lib/src/main/resources/static/tool-test-console.html @@ -36,7 +36,7 @@
- AXHUB WAS + AXHUB Gateway