Compare commits

...

91 Commits

Author SHA1 Message Date
jade
7dca243380 fix: add io.shinhanlife.dap.common.integration to component scan paths across all application modules 2026-07-21 17:09:46 +09:00
jade
c90654bd1b refactor: move AxhubMciComponent and GlowMockConfig to core module for common usage 2026-07-21 17:07:02 +09:00
jade
334af3e40e fix: register GlowMciComponent as Spring Bean via config to resolve IntelliJ autowiring error 2026-07-21 17:02:57 +09:00
jade
ea27ed621a fix: resolve IntelliJ autowiring and inspection warnings for AxhubMciComponent 2026-07-21 16:59:52 +09:00
jade
fbe7091975 refactor: rename LicoMciComponent to AxhubMciComponent 2026-07-21 16:57:10 +09:00
jade
7ccca1812f refactor: apply GlowMciComponent architecture as requested by Shinhan Life standard 2026-07-21 16:53:31 +09:00
jade
123fdb30a9 fix: correct Onnba3011ReqDto fields from new API spec and fix OnnbaMciToolService compile error 2026-07-21 16:42:43 +09:00
jade
0ed927f845 feat: update Onnba3011ReqDto fields from API spec 2026-07-21 16:38:54 +09:00
jade
60ac3bc4be refactor: extract Onnba3011ReqDto into separate file 2026-07-21 16:32:40 +09:00
jade
0afa489165 feat: add OnnbaMciToolService converted from legacy MCI code 2026-07-21 15:56:37 +09:00
jade
8cd07a255c fix: add sample category to pre-initialized servers to avoid 400 bad request 2026-07-21 15:13:31 +09:00
jade
cc98d173ad fix: set register=true for sample tools to enable auto-registration on Render 2026-07-21 15:04:25 +09:00
jade
c450f26d53 fix: correct gateway URL in application-dev.properties for all tools 2026-07-21 14:55:06 +09:00
jade
7660db3c38 fix: disable auto namespace prefix in PodScaffolder 2026-07-21 14:24:49 +09:00
jade
beed6e7001 docs: add sample category to scaffolder and readme 2026-07-21 14:22:22 +09:00
jade
8612dd6ea7 feat: rename sample tools 2026-07-21 14:19:08 +09:00
jade
db6ed5b09a feat: add get_exchange_rate and get_daily_quote tools and set categoryKey to sample 2026-07-21 14:17:36 +09:00
jade
db949be3f3 feat: generate and log new request-id for outbound HTTP calls in WeatherTool 2026-07-21 14:12:58 +09:00
jade
9edcdab450 관련 소스 수정 2026-07-21 14:05:39 +09:00
jade
82b1e6e267 feat: rename tool get_weather to weather 2026-07-21 14:04:48 +09:00
jade
e35b378d61 fix: wrap execute tool response to match expected client format 2026-07-21 14:00:51 +09:00
jade
f8d6ed8da3 feat: remove namespace prefix from all remaining tool modules 2026-07-21 13:52:37 +09:00
jade
437d061bef feat: remove namespace prefix from dap-tool-other 2026-07-21 13:50:54 +09:00
jade
9cce77a142 fix: add missing register=false to register_vacation tool 2026-07-21 13:45:03 +09:00
jade
115aafa220 fix: swap trace-id and request-id header mapping 2026-07-21 13:43:13 +09:00
jade
da7ed2e28b fix: remove duplicate register property in TemplateUtilityService 2026-07-21 13:32:40 +09:00
jade
48ac7b9f9f chore: disable redis heartbeat registration for all tools (register=false) 2026-07-21 13:26:02 +09:00
jade
580edd98a6 feat: add trace-id and request-id header logging to tool controller 2026-07-21 13:18:38 +09:00
jade
b9966bbbda feat: rename weather tool from get_current_weather to get_weather 2026-07-21 13:10:52 +09:00
jade
438e30f987 refactor: change internal gateway-to-tool routing from JSON-RPC to REST API 2026-07-21 12:50:28 +09:00
jade
fe9e00b5fb feat: add missing globalId to SlCommonHeaderDto 2026-07-21 10:48:13 +09:00
jade
b0f12b578d feat: add SL and OL standard header DTOs from spec 2026-07-21 10:44:17 +09:00
jade
39513a1470 feat: apply full spec to ShinhanCommonHeaderDto (42 fields) 2026-07-21 10:36:34 +09:00
jade
821b249d50 fix: resolve MCI sample tool execution errors and improve parameter definitions in UI 2026-07-21 10:29:30 +09:00
jade
e02a4e24e8 fix: Add dap-tool-core to dap-gateway Dockerfile 2026-07-21 09:06:34 +09:00
jade
6b8ca5581f fix: Remove COPY dap-common from Dockerfiles 2026-07-21 09:04:07 +09:00
jade
d6869f8191 refactor: Merge dap-common into dap-tool-core 2026-07-21 09:02:48 +09:00
jade
515ab409ec fix: Update BusinessToolController to use generateSchema 2026-07-20 17:15:49 +09:00
jade
b55f863de9 fix: Complete JsonSchemaGenerator with required properties for MCP compliance 2026-07-20 17:07:39 +09:00
jade
80ff392b5a feat: Add SampleMciToolService for MCI integration sample 2026-07-20 16:44:12 +09:00
jade
6dd1fe6cda refactor: apply Glow Framework package naming conventions (mcg, mcc)
- io.shinhanlife.dap.biz.mcp.gateway -> io.shinhanlife.dap.mcg
- io.shinhanlife.dap.biz.mcp.tool -> io.shinhanlife.dap.mcc
- io.shinhanlife.dap.biz.mcp.adapter -> io.shinhanlife.dap.common.adapter
- io.shinhanlife.dap.biz.mcp.sample -> io.shinhanlife.dap.mcc.sample
2026-07-20 11:39:36 +09:00
jade
2162587a46 fix: 딥 빌더 테스트 도구 실행 시 system 권한 도메인 허용 설정 추가 2026-07-20 10:28:11 +09:00
jade
736593c516 fix: Deep Agent Builder 무한 대기 현상 해결을 위해 JSON-RPC Notification 처리 후 SSE 강제 종료 2026-07-20 10:15:07 +09:00
jade
bd85ca9f88 fix: Deep Agent Builder JSON 파싱 에러 방지를 위해 커스텀 SSE 엔드포인트 이벤트 제거 2026-07-20 09:53:44 +09:00
jade
d6801b7ba0 fix: Deep Agent Builder 호환성을 위한 mcp-http Warm Pool 세션 버그 및 CORS 헤더 수정 2026-07-20 09:36:15 +09:00
jade
2e6b28e1c1 log 경로 수정 2026-07-20 09:01:44 +09:00
jade
0ebac4e375 McpFunction에 MCP 표준 힌트 4개 추가 2026-07-17 18:08:12 +09:00
jade
8359371122 필요 없는 부분 삭제 2026-07-17 17:38:13 +09:00
jade
7eef3a65c5 refactor: Fix all inline FQ names with proper imports and add Javadoc to 67 files 2026-07-17 15:20:26 +09:00
jade
467289bf9f skill 추가 2026-07-17 01:06:51 +09:00
jade
c05bfb0ab3 skill 추가 2026-07-17 01:05:27 +09:00
jade
ac15d38468 skill 추가 2026-07-17 01:01:24 +09:00
jade
87f367b2c1 git 변경 테스트 2026-07-17 00:50:06 +09:00
jade
5a5e67629d chore: Add explicit import for CustomWebMvcSseServerTransportProvider 2026-07-16 21:37:57 +09:00
jade
e1591761d2 refactor: Replace inline fully qualified package names with proper imports 2026-07-16 21:36:51 +09:00
jade
c982696631 git 변경 테스트 2026-07-16 21:25:40 +09:00
jade
2c0c0a3790 fix: Rename AXHUB-Gateway to DAP-Gateway in DynamicMcpServerManager 2026-07-16 21:11:11 +09:00
jade
993ced0813 docs: Update README.md to reflect new DAP architecture and module renames 2026-07-16 20:59:32 +09:00
jade
abd2766577 fix: Actually restore correct UTF-8 bytes for README.md 2026-07-16 20:56:52 +09:00
jade
046c9dee75 fix: Restore and convert README.md to UTF-8 2026-07-16 20:55:11 +09:00
jade
0225d5e8a4 git 변경 테스트 2026-07-16 20:27:01 +09:00
jade
746879e804 git 변경 테스트 2026-07-16 20:22:27 +09:00
jade
5a0e6951e2 Refactor: Rename Application classes to Dap*Application prefix 2026-07-16 20:07:52 +09:00
jade
9b0ed9dd51 수정 버전111 2026-07-16 18:03:59 +09:00
jade
1a572351c3 수정 버전111 2026-07-16 17:50:27 +09:00
jade
979f8e0c3d 수정 버전 2026-07-16 17:38:49 +09:00
jade
a82bc1a196 refactor: 어플리케이션 업무코드(DAP) 전면 전환에 따른 패키지 및 모듈명 대규모 리팩토링 2026-07-16 16:18:04 +09:00
jade
6159b0856a refactor: 어플리케이션 업무코드를 AXH에서 DAP로 변경 반영 2026-07-16 16:04:21 +09:00
jade
931f182446 fix: ShinhanCommonHeaderDto 누락된 envrTypeCd 필드 추가 및 불필요한 대외망(FixedLength) 필드 정리 2026-07-16 15:25:11 +09:00
jade
dddb36daa8 docs: 대외MCI(FixedLength) 범위 제외 및 AGENTS.md 규칙 최신화 2026-07-16 15:10:08 +09:00
jade
b6827f1fb9 fix: CustomWebMvcSseServerTransportProvider 클래스에 누락된 @Slf4j 어노테이션 추가 2026-07-16 14:36:48 +09:00
jade
64ebed8ad9 fix: 프로젝트 최상위 각종 설정 파일(gradlew 등)에 잘못 추가되었던 프로퍼티 내용 완전 제거 2026-07-16 14:30:30 +09:00
jade
8e2c44a1b4 fix: 최상위 build.gradle 파일에 잘못 병합되었던 프로퍼티 제거 2026-07-16 14:23:32 +09:00
jade
6d24538b6e fix: settings.gradle 파일에 잘못 추가된 프로퍼티 내용 제거 2026-07-16 14:17:55 +09:00
jade
911c7f43a5 fix: Java 파일 내 보이지 않는 BOM(Byte Order Mark) 문자 제거하여 컴파일 오류 해결 2026-07-16 14:15:57 +09:00
jade
c9b3baacff style: import 구문 위치 및 누락된 패키지 참조 정리 2026-07-16 14:09:33 +09:00
jade
608481180d refactor: MciTestController 내부 패키지 경로를 풀네임 대신 import로 명시 2026-07-16 14:08:10 +09:00
jade
77ab7e48a3 fix: IntelliJ Bean Autowire 경고 해결 (@EnableConfigurationProperties 추가) 2026-07-16 14:06:38 +09:00
jade
93947d3360 docs: 신한라이프 EAI/MCI 구간별 통신 프로토콜 규칙 AGENTS.md에 추가 2026-07-16 13:08:13 +09:00
jade
5ca3753342 refactor: ShinhanMciSender 위치를 axhub-tool-core 어댑터 레이어로 이동 2026-07-16 13:03:12 +09:00
jade
13d1981efd fix: properties 파일 내 한글 깨짐 현상 수정 (UTF-8 인코딩 적용) 2026-07-16 12:55:05 +09:00
jade
efa9c69f0a feat: 신한라이프 대내MCI/EAI JSON 연계 표준(ShinhanMciSender) 신규 구현 2026-07-16 12:47:50 +09:00
jade
390eddb98b chore: remove test payload file 2026-07-16 11:00:46 +09:00
jade
e5f6321eed fix: resolve MCP Gateway SSE hanging issue by replacing RouterFunction with SseEmitter in RestController 2026-07-16 10:56:13 +09:00
jade
6d49edfc2b 수정 버전 2026-07-16 09:19:32 +09:00
jade
5e97e6754f fix: AI 채팅 툴 실행 시 동적 Tenant ID(Agent ID)를 주입하여 권한 거부 오류 해결 2026-07-16 09:07:38 +09:00
jade
d70a81ee43 fix: ensure ssePath for common uses /mcp/sse/common 2026-07-15 23:10:32 +09:00
jade
0323ce4f2c fix: update CORS config to allow all origin patterns for Deep Agent Builder integration 2026-07-15 23:01:55 +09:00
jade
c6edd671e2 fix: pre-initialize dynamic mcp servers so endpoints are always open even when no tools are synced 2026-07-15 22:50:35 +09:00
jade
2846dacab0 feat: implement dynamic mcp server routing to bypass SSE deprecation and 50 tools limit 2026-07-15 22:39:21 +09:00
jade
5f908c3e53 git 변경 테스트 2026-07-15 22:13:45 +09:00
340 changed files with 5373 additions and 2283 deletions

View File

@@ -26,9 +26,7 @@
* *
* </pre> * </pre>
*/ */
* 신한라이프 인터페이스 통신 프로토콜 및 메시지 포맷 가이드 (준버티컬 Tool 대상):
* 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외)
* 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행)
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.

View File

@@ -1,33 +0,0 @@
# AX HUB 프로젝트 개발 규칙 (AGENTS.md)
이 파일은 AI Agent(Antigravity)가 이 프로젝트에서 작업할 때 항상 명심하고 지켜야 할 규칙을 정의하는 파일입니다.
작업 시 유의해야 할 사항이 생기면 언제든지 이 아래에 자유롭게 내용을 추가해 주세요!
## 💡 개발 가이드라인
* 패키지명은 `controller` 대신 `presentation`을 사용합니다.
* MapStruct 사용 시, 테스트 환경 에러를 방지하기 위해 생성자(`new ...Impl()`) 대신 `Mappers.getMapper(인터페이스명.class)` 방식으로 인스턴스를 가져옵니다.
## 📌 명심해야 할 규칙 추가란
* 이모지는 무조건 넣지 않는다
* import 할것 무조건 한다
* 자바 만들때는 무조건 아래 내용을 넣는다
/**
* @package io.shinhanlife.axhub.biz.mcp.tool.sms
* @className AxHubToolSmsApplication
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/

View File

@@ -0,0 +1,16 @@
---
name: chaos-tool-tester
description: 현재 연결된 MCP 도구들에 대해 엣지 케이스(Null, 타입 오류, SQL 인젝션 문자열 등)를 고의로 주입하여 안정성을 테스트합니다.
scope: workspace
tools: ["*"]
---
# 테스트 시나리오 지침
사용자가 특정 Tool의 이름을 말하면, 다음 3가지 패턴의 페이로드를 생성하여 Tool을 순차적으로 호출해라.
1. **타입 브레이커:** Integer가 들어가야 할 곳에 긴 String 넣기
2. **누락 테스트:** 필수(Required) 파라미터를 누락시키고 호출하기
3. **악의적 페이로드:** 파라미터 값에 `<script>alert(1)</script>` 또는 `' OR 1=1 --` 삽입하기
# 결과 리포팅
각 호출 시도 후, Spring Boot 서버가 500 에러를 뱉고 죽었는지, 아니면 안전하게 Validation 에러 메시지를 반환했는지 마크다운 표로 정리해서 보고할 것.

View File

@@ -0,0 +1,12 @@
---
name: gateway-routing-tracer
description: 게이트웨이 로그를 분석하여 특정 Tool Call이나 Request ID의 라우팅 경로와 소요 시간을 추적합니다.
scope: workspace
---
# 실행 스크립트
이 스킬은 같은 폴더에 위치한 `trace.sh` 스크립트를 실행하여 로그를 파싱합니다.
# 에이전트 지침
1. 사용자가 조회하고자 하는 `Request ID` 또는 `Tool 이름`을 감지하면, 백그라운드 터미널에서 `trace.sh <인자>`를 실행해라.
2. 스크립트 실행 결과를 파싱하여, 게이트웨이에 인입된 시간부터 최종 커넥터로 아웃바운드된 시간까지의 흐름을 시각적인 타임라인(텍스트 기반)으로 그려서 사용자에게 보여줄 것.

View File

@@ -0,0 +1,18 @@
#!/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}'

View File

@@ -0,0 +1,16 @@
---
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 형태로만 출력하고, 불필요한 부연 설명은 생략한다.

View File

@@ -0,0 +1,249 @@
# 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

View File

@@ -0,0 +1,602 @@
# 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
<evaluation>
<qa_pair>
<question>Your question here</question>
<answer>Single verifiable answer</answer>
</qa_pair>
</evaluation>
```
---
## 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
<evaluation>
<qa_pair>
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
<answer>Website Redesign</answer>
</qa_pair>
<qa_pair>
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
<answer>sarah_dev</answer>
</qa_pair>
<qa_pair>
<question>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?</question>
<answer>7</answer>
</qa_pair>
<qa_pair>
<question>Find the repository with the most stars that was created before 2023. What is the repository name?</question>
<answer>data-pipeline</answer>
</qa_pair>
</evaluation>
```
## Evaluation Examples
### Good Questions
**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**
```xml
<qa_pair>
<question>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?</question>
<answer>Python</answer>
</qa_pair>
```
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
<qa_pair>
<question>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?</question>
<answer>Product Manager</answer>
</qa_pair>
```
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
<qa_pair>
<question>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.</question>
<answer>alex_eng</answer>
</qa_pair>
```
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
<qa_pair>
<question>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?</question>
<answer>Healthcare</answer>
</qa_pair>
```
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
<qa_pair>
<question>How many open issues are currently assigned to the engineering team?</question>
<answer>47</answer>
</qa_pair>
```
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
<qa_pair>
<question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
<answer>developer123</answer>
</qa_pair>
```
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
<qa_pair>
<question>List all the repositories that have Python as their primary language.</question>
<answer>repo1, repo2, repo3, data-pipeline, ml-tools</answer>
</qa_pair>
```
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 `<qa_pair>`** 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 `<qa_pair>` elements:
```xml
<evaluation>
<qa_pair>
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
<answer>Website Redesign</answer>
</qa_pair>
<qa_pair>
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
<answer>sarah_dev</answer>
</qa_pair>
</evaluation>
```
## 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
<evaluation>
<qa_pair>
<question>Find the user who created the most issues in January 2024. What is their username?</question>
<answer>alice_developer</answer>
</qa_pair>
<qa_pair>
<question>Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name.</question>
<answer>backend-api</answer>
</qa_pair>
<qa_pair>
<question>Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take?</question>
<answer>127</answer>
</qa_pair>
</evaluation>
```
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

6
.gitattributes vendored
View File

@@ -1,12 +1,12 @@
# 기본값: 모든 텍스트 파일 CRLF (Windows 개발 환경) # 湲곕낯媛? 紐⑤뱺 ?띿뒪???뚯씪 CRLF (Windows 媛쒕컻 ?섍꼍)
* text=auto eol=crlf * text=auto eol=crlf
# Shell 스크립트는 LF 고정 (Linux CI/CD 실행 환경) # Shell ?ㅽ겕由쏀듃??LF 怨좎젙 (Linux CI/CD ?ㅽ뻾 ?섍꼍)
*.sh text eol=lf *.sh text eol=lf
mvnw text eol=lf mvnw text eol=lf
gradlew text eol=lf gradlew text eol=lf
# 바이너리 파일 (줄바꿈 변환 제외) # 諛붿씠?덈━ ?뚯씪 (以꾨컮轅?蹂€???쒖쇅)
*.jar binary *.jar binary
*.war binary *.war binary
*.ear binary *.ear binary

6
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# ===== 빌드 결과물 ===== # ===== 鍮뚮뱶 寃곌낵臾?=====
target/ target/
!**/src/main/**/target/ !**/src/main/**/target/
!**/src/test/**/target/ !**/src/test/**/target/
@@ -9,7 +9,7 @@ target/
*.ear *.ear
*.nar *.nar
# ===== 로그 ===== # ===== 濡쒓렇 =====
*.log *.log
logs/ logs/
spring-shell.log spring-shell.log
@@ -61,6 +61,6 @@ build/
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
# ===== 환경설정 (민감정보 분리 시) ===== # ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? =====
# application-local.properties # application-local.properties
# application-secret.properties # application-secret.properties

View File

@@ -12,14 +12,14 @@ public class AddJavadoc {
"/**\n" + "/**\n" +
" * @package %s\n" + " * @package %s\n" +
" * @className %s\n" + " * @className %s\n" +
" * @description AX HUB 시스템 처리 클래스\n" + " * @description AX HUB ?쒖뒪??泥섎━ ?대옒??n" +
" * @author 김형식\n" + " * @author 源€?뺤떇\n" +
" * @create 2026.09.01\n" + " * @create 2026.09.01\n" +
" * <pre>\n" + " * <pre>\n" +
" * ---------- 개정이력 ----------\n" + " * ---------- 媛쒖젙?대젰 ----------\n" +
" * 수정일 수정자 수정내용\n" + " * ?섏젙?? ?섏젙?? ?섏젙?댁슜\n" +
" * ---------- -------- ---------------------------\n" + " * ---------- -------- ---------------------------\n" +
" * 2026.09.01 김형식 최초생성\n" + " * 2026.09.01 源€?뺤떇 理쒖큹?앹꽦\n" +
" * \n" + " * \n" +
" * </pre>\n" + " * </pre>\n" +
" */"; " */";
@@ -50,7 +50,7 @@ public class AddJavadoc {
List<String> lines = Files.readAllLines(file); List<String> lines = Files.readAllLines(file);
String content = String.join("\n", lines); String content = String.join("\n", lines);
if (content.contains("---------- 개정이력 ----------") || content.contains("@className")) { if (content.contains("---------- 媛쒖젙?대젰 ----------") || content.contains("@className")) {
System.out.println("Skipping (already has javadoc): " + file); System.out.println("Skipping (already has javadoc): " + file);
return; return;
} }

View File

@@ -1,30 +1,30 @@
# 1. 빌드 환경 (JDK 21) # 1. ??š®ë±???<3F>ê¼<C3AA> (JDK 21)
FROM eclipse-temurin:21-jdk-alpine AS builder FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app WORKDIR /app
# Gradle 래퍼와 소스 복사 # Gradle ??ë<E28098><C3AB>?? ???’ª 蹂듭ê¶?
COPY gradlew . COPY gradlew .
COPY gradle gradle COPY gradle gradle
COPY build.gradle settings.gradle ./ COPY build.gradle settings.gradle ./
COPY src src COPY src src
# 권한 부여 및 빌드 (테스트 제외) # æ²…ëš°ë¸??ºÂ€??è«???š®ë±?(???’ª????–쇅)
RUN chmod +x gradlew RUN chmod +x gradlew
RUN ./gradlew clean build -x test RUN ./gradlew clean build -x test
# 2. 실행 환경 (JRE 21) # 2. ??½ë»¾ ??<3F>ê¼<C3AA> (JRE 21)
FROM eclipse-temurin:21-jre-alpine FROM eclipse-temurin:21-jre-alpine
WORKDIR /app WORKDIR /app
# 타임존 설정 (한국 시간) # ???꾩ã€???¼ì ™ (??“뎅 ??“ì»™)
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul ENV TZ=Asia/Seoul
# 빌드된 JAR 파일 복사 # ??š®ë±??JAR ???”ª 蹂듭ê¶?
COPY --from=builder /app/build/libs/*.jar app.jar COPY --from=builder /app/build/libs/*.jar app.jar
# 기본 포트 노출 # 湲곕???????몄텧
EXPOSE 8081 EXPOSE 8081
# 컨테이너 실행 시 JAR 실행 # ?Œâ‘¦???€ê¼???½ë»¾ ??JAR ??½ë»¾
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -29,4 +29,3 @@ Due to Maven's design, elements are inherited from the parent POM to the project
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent. While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
To prevent this, the project POM contains empty overrides for these elements. To prevent this, the project POM contains empty overrides for these elements.
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.

View File

@@ -1,3 +1,17 @@
/**
* @package io.shinhanlife
* @className McpBridge
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;
@@ -21,7 +35,7 @@ public class McpBridge {
try { try {
HttpRequest req = HttpRequest.newBuilder() HttpRequest req = HttpRequest.newBuilder()
// Spring AI MCP Server의 공식 단일 엔드포인트 // Spring AI MCP Server??怨듭떇 ?⑥씪 ?붾뱶?ъ씤??
.uri(URI.create("http://localhost:8081/mcp")) .uri(URI.create("http://localhost:8081/mcp"))
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(line)) .POST(HttpRequest.BodyPublishers.ofString(line))
@@ -31,7 +45,7 @@ public class McpBridge {
System.out.println(response.body()); System.out.println(response.body());
System.out.flush(); System.out.flush();
} catch (Exception e) { } catch (Exception e) {
// MCP 표준 에러 포맷으로 반환 (임의로 id 추출 제외, 최소한의 에러 응답) // MCP ?쒖? ?먮윭 ?щ㎎?쇰줈 諛섑솚 (?꾩쓽濡?id 異붿텧 ?쒖쇅, 理쒖냼?쒖쓽 ?먮윭 ?묐떟)
System.out.println("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}}"); System.out.println("{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32603,\"message\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}}");
System.out.flush(); System.out.flush();
} }
@@ -41,6 +55,3 @@ public class McpBridge {
} }
} }
} }

108
README.md
View File

@@ -1,15 +1,15 @@
# AXHUB Backend # DAP Backend
Spring Boot 기반 AXHUB 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다. Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다.
--- ---
## 아키텍처 개요 (Architecture Overview) ## 아키텍처 개요 (Architecture Overview)
AXHUB Backend는 크게 2개의 주요 애플리케이션 계층으로 분리 운영됩니다: DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
1. **MCP Gateway (`AxHubGatewayApplication`)**: 내장된 웹 UI(AI 챗봇, 스캐폴더) 제공 및 외부 LLM(Claude, GPT 등) 서버의 MCP 통신을 받아 내부 Tool 서버들로 분배(라우팅)하는 허브 서버 (포트: 8081) 1. **MCP Gateway (`DapGatewayApplication`)**: 외부 LLM(Claude, GPT 등) 서버의 MCP 통신을 받아, 내부 Tool 서버들로 분배(라우팅)하는 허브 서버이자 관리자 웹(Scaffolder)을 제공하는 통합 서버 (포트: 8081)
2. **MCP Tool (`AxHubToolApplication`)**: 실제 레거시 시스템(MCI, EAI 등)과 통신하여 비즈니스 로직(결제, 휴가신청 등)을 수행하는 어댑터 서버 (포트: 8082~8085 등 분산 구성 가능) 2. **MCP Tool (`DapTool*Application`)**: 실제 레거시 시스템(MCI, EAI 등)과 통신하여 비즈니스 로직(결제, 휴가신청 등)을 수행하는 어댑터 서버 (포트: 8082~8085 등 분산 구성 가능)
--- ---
@@ -30,60 +30,48 @@ AXHUB Backend는 크게 2개의 주요 애플리케이션 계층으로 분리
## ▶ 실행 방법 (How to Run) ## ▶ 실행 방법 (How to Run)
### 1. Docker Compose를 이용한 전체 실행 (권장) ### 1. Gateway & Tool 서버 실행 (MCP 연동용)
마이크로서비스 아키텍처 특성상 여러 개의 Tool Pod이 필요하므로, Docker Compose를 이용한 전체 실행을 권장합니다.
```bash
# 전체 시스템(Gateway + Redis + 모든 Tool Pod) 빌드 및 백그라운드 실행
./gradlew build -x test
docker compose up -d --build
```
- **Gateway (Chat UI & 라우터)**: `http://localhost:8281`
- Gateway가 뜨면 내장된 챗봇 웹 UI(`http://localhost:8281/chat.html`)에 접속하여 바로 테스트할 수 있습니다.
### 2. 로컬 개발 시 개별 실행 (IntelliJ / Gradle)
개발 중 특정 모듈만 띄워 디버깅해야 할 경우 아래와 같이 실행합니다.
- **Gateway 서버 기동:** - **Gateway 서버 기동:**
- `./gradlew :axhub-gateway:bootRun` - `./gradlew :dap-gateway:bootRun`
- **Tool 서버 기동 (예: other 툴):** - **Tool 서버 기동:**
- `./gradlew :axhub-tool-other:bootRun` - `./gradlew :dap-tool-other:bootRun` (또는 dap-tool-payment 등)
- Tool 서버가 기동되면 자동으로 Gateway에 자신을 등록(Auto-Registration)합니다. - Tool 서버가 기동되면 자동으로 Gateway(8081)에 자신을 등록(Auto-Registration)합니다.
- **(선택) 특정 Tool 그룹만 실행하기:**
- 업무 특성에 따라 세분화된 그룹에 속한 Tool만 띄우고 싶다면, 실행 인수에 `--mcp.tool.target=그룹명`을 추가합니다.
- **지원되는 그룹명:**
- `NOTIFICATION`: 이메일, SMS 발송
- `CLAIM`: 청구 처리, 심사 상태 조회
- `POLICY`: 증권 발행, 발행 가능 여부 조회
- `HR`: 휴가 등록, 연차 갯수 조회
- `CONTRACT`: 계약 상태, 계약 상세 조회
- `CUSTOMER`: 고객 등급, 고객 상세 정보 조회
- `SAMPLE`: 날씨, 환율, 명언 조회 등 외부 연동 샘플
- IntelliJ IDEA: `Run/Debug Configurations`에서 `DapTool*Application``Program arguments``--mcp.tool.target=NOTIFICATION` 입력
--- ---
## 🤖 AI Agent 연동 아키텍처 (Spring AI & MCP) ## 🤖 AI Agent 연동 아키텍처 (MCP & Agent Builder)
본 시스템은 **투트랙(Two-Track) AI 연동 아키텍처**를 제공하여 로컬 개발 환경과 프로덕션 환경 모두를 완벽하게 지원합니다. 본 시스템은 **투트랙(Two-Track) AI 연동 아키텍처**를 제공하여 로컬 개발 환경과 프로덕션 환경 모두를 완벽하게 지원합니다.
### 1. 내장형 웹 챗봇 (Spring AI 기반) - NEW! 🎉 ### 1. 로컬 코딩 AI (Antigravity, Cursor, Claude Desktop 등) 연동
가장 빠르고 직관적으로 AI 에이전트를 테스트할 수 있는 내장형 챗봇 화면을 제공합니다. Gateway 서버 자체에 **Spring AI (spring-ai-starter-mcp-server-webmvc)** 가 연동되어 있어 별도의 파이썬 스크립트나 외부 앱 없이도 즉각적인 테스트가 가능합니다. 표준 MCP 통신(Stdio)을 요구하는 로컬 AI 에이전트를 위해 자바 기반의 브릿지 스크립트(`McpBridge.java`)를 내장하고 있습니다. 브릿지가 Stdio 요청을 HTTP로 변환하여 로컬 환경의 Gateway(포트: 8281)로 전달합니다.
- **접속 방법**: Gateway(Docker) 기동 후 브라우저에서 `http://localhost:8281/chat.html` 접속
- **동작 방식**:
1. 사용자가 질문을 입력하면 내부 `ChatClient` (Gemini API 등)로 전송
2. Spring AI가 내부 레지스트리의 Tool 목록을 분석하여 필요한 Tool 탐색
3. LLM이 Tool 호출 판단 시, Gateway의 `ExecuteService`를 거쳐 Tool Pod의 기능을 직접 실행
4. 결과를 LLM이 다시 해석하여 사용자에게 자연어로 응답
### 2. 프로덕션 클라우드 AI (Google Cloud Agent Builder 등) 연동
실제 라이브 서비스에서 동작하는 클라우드 Agent Builder는 REST API 기반의 OpenAPI Spec을 요구합니다.
`axhub-gateway`는 이미 **Agent Builder 규격의 REST API(`/mcp/api/v1/tools/call`)를 네이티브로 제공**하므로, 별도의 브릿지나 어댑터 없이 Endpoint URL과 Swagger(OpenAPI) 문서만 클라우드 콘솔에 등록하면 즉시 라이브 챗봇/에이전트로 서비스할 수 있습니다.
### 3. (Legacy) 로컬 코딩 AI (Cursor, Claude Desktop 등) 연동
표준 MCP 통신(Stdio)을 요구하는 로컬 AI 에이전트를 위해 자바 기반의 브릿지 스크립트(`McpBridge.java`)를 내장하고 있습니다. 브릿지가 Stdio 요청을 HTTP로 변환하여 로컬 환경의 Gateway로 전달합니다.
- **설정 방법**: IDE의 `mcp_config.json` 설정 파일에 아래와 같이 등록합니다. - **설정 방법**: IDE의 `mcp_config.json` 설정 파일에 아래와 같이 등록합니다.
```json ```json
"mcpServers": { "mcpServers": {
"axhub-gateway": { "dap-gateway": {
"command": "java", "command": "java",
"args": ["C:/절대경로/axhub-backend-main/McpBridge.java"] "args": ["C:/절대경로/dap-backend-main/McpBridge.java"]
} }
} }
``` ```
- **특정 카테고리 툴 필터링**: `McpBridge.java` 내부의 URI 파라미터(`?categoryKey=sample` 등)를 수정하여 원하는 도메인의 툴만 선택적으로 AI에게 학습시킬 수 있습니다.
### 2. 프로덕션 클라우드 AI (Google Cloud Agent Builder 등) 연동
실제 라이브 서비스에서 동작하는 클라우드 Agent Builder는 REST API 기반의 OpenAPI Spec을 요구합니다.
`dap-gateway`는 이미 **Agent Builder 규격의 REST API(`/mcp/api/v1/tools/call`)를 네이티브로 제공**하므로, 별도의 브릿지나 어댑터 없이 Endpoint URL과 Swagger(OpenAPI) 문서만 클라우드 콘솔에 등록하면 즉시 라이브 챗봇/에이전트로 서비스할 수 있습니다.
--- ---
@@ -123,9 +111,9 @@ MSA 및 외부 시스템(MCI) 연동 환경의 안정성을 위해 완벽한 3-T
## 모듈(Pod) 및 Tool 코드 자동 생성 (Scaffolders) ## 모듈(Pod) 및 Tool 코드 자동 생성 (Scaffolders)
새로운 도메인의 기능을 추가할 때 발생하는 반복적인 설정(보일러플레이트, 설정 파일 복사 등)을 1초 만에 자동화하기 위해 **AXHUB Developer Portal (Web UI)** 및 **CLI 스캐폴더 2종**을 제공합니다. 새로운 도메인의 기능을 추가할 때 발생하는 반복적인 설정(보일러플레이트, 설정 파일 복사 등)을 1초 만에 자동화하기 위해 **DAP Developer Portal (Web UI)** 및 **CLI 스캐폴더 2종**을 제공합니다.
### 1. AXHUB Developer Portal (Web UI) - 가장 추천하는 방식! ### 1. DAP Developer Portal (Web UI) - 가장 추천하는 방식!
이제 더 이상 터미널에서 명령어를 칠 필요가 없습니다. Gateway 모듈에 내장된 웹 화면에서 빈칸만 채우면 코드가 마법처럼 찍혀 나옵니다. 이제 더 이상 터미널에서 명령어를 칠 필요가 없습니다. Gateway 모듈에 내장된 웹 화면에서 빈칸만 채우면 코드가 마법처럼 찍혀 나옵니다.
1. **접속 방법**: Gateway 서버 기동 후 브라우저에서 `http://localhost:8081/admin/scaffold.html` 접속 1. **접속 방법**: Gateway 서버 기동 후 브라우저에서 `http://localhost:8081/admin/scaffold.html` 접속
@@ -140,11 +128,11 @@ MSA 및 외부 시스템(MCI) 연동 환경의 안정성을 위해 완벽한 3-T
```bash ```bash
# 사용법: javac로 컴파일 후 실행 # 사용법: javac로 컴파일 후 실행
javac -encoding UTF-8 axhub-common/src/main/java/io/shinhanlife/axhub/common/util/PodScaffolder.java javac -encoding UTF-8 dap-common/src/main/java/io/shinhanlife/dap/common/util/PodScaffolder.java
java -cp axhub-common/src/main/java io.shinhanlife.axhub.common.util.PodScaffolder [모듈명] [포트번호] java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.PodScaffolder [모듈명] [포트번호]
# 실행 예시 (axhub-tool-hr 모듈을 8086 포트로 생성) # 실행 예시 (dap-tool-hr 모듈을 8086 포트로 생성)
java -cp axhub-common/src/main/java io.shinhanlife.axhub.common.util.PodScaffolder hr 8086 java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.PodScaffolder hr 8086
``` ```
### 2⃣ 생성된 모듈에 새로운 툴(Function)을 추가할 때: `ToolScaffolder` ### 2⃣ 생성된 모듈에 새로운 툴(Function)을 추가할 때: `ToolScaffolder`
@@ -152,11 +140,11 @@ java -cp axhub-common/src/main/java io.shinhanlife.axhub.common.util.PodScaffold
```bash ```bash
# 사용법: javac로 컴파일 후 실행 # 사용법: javac로 컴파일 후 실행
javac -encoding UTF-8 axhub-common/src/main/java/io/shinhanlife/axhub/common/util/ToolScaffolder.java javac -encoding UTF-8 dap-common/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java
java -cp axhub-common/src/main/java io.shinhanlife.axhub.common.util.ToolScaffolder [Tool이름] [인터페이스ID] "[기능설명]" "[그룹명]" "[통신방식]" "[모듈명]" java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.ToolScaffolder [Tool이름] [인터페이스ID] "[기능설명]" "[그룹명]" "[통신방식]" "[모듈명]"
# 실행 예시 (payment 모듈에 결제 승인 기능 추가) # 실행 예시 (payment 모듈에 결제 승인 기능 추가)
java -cp axhub-common/src/main/java io.shinhanlife.axhub.common.util.ToolScaffolder PaymentApproval PAY_001 "결제 승인 처리 기능" "COMMON" "HTTP" "axhub-tool-payment" java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.ToolScaffolder PaymentApproval PAY_001 "결제 승인 처리 기능" "COMMON" "HTTP" "dap-tool-payment"
``` ```
--- ---
@@ -164,14 +152,14 @@ java -cp axhub-common/src/main/java io.shinhanlife.axhub.common.util.ToolScaffol
## 패키지 구조 (Package Structure) ## 패키지 구조 (Package Structure)
```text ```text
axhub-backend-main (Root) dap-backend-main (Root)
├── axhub-gateway # MCP 라우팅 허브 서버 (외부 LLM과 통신 및 Tool 분배) ├── dap-gateway # MCP 라우팅 허브 서버 (외부 LLM과 통신 및 Tool 분배)
├── axhub-common # 공통 모듈 (Security, Session, Config 등) ├── dap-common # 공통 모듈 (Security, Session, Config 등)
├── axhub-tool-core # Tool 공통 기능 (AbstractMcpToolService, Annotation, Scaffolder) ├── dap-tool-core # Tool 공통 기능 (AbstractMcpToolService, Annotation, Scaffolder)
├── axhub-tool-email # [Tool] 이메일 발송 특화 어댑터 모듈 ├── dap-tool-email # [Tool] 이메일 발송 특화 어댑터 모듈
├── axhub-tool-sms # [Tool] SMS 발송 특화 어댑터 모듈 ├── dap-tool-sms # [Tool] SMS 발송 특화 어댑터 모듈
├── axhub-tool-payment # [Tool] 결제 비즈니스 어댑터 모듈 (Scaffolded) ├── dap-tool-payment # [Tool] 결제 비즈니스 어댑터 모듈 (Scaffolded)
└── axhub-tool-other # [Tool] 기타 비즈니스(청구, 계약, 고객, HR 등) 어댑터 모듈 └── dap-tool-other # [Tool] 기타 비즈니스(청구, 계약, 고객, HR 등) 어댑터 모듈
``` ```
*(참고: 기존 단일 모듈 프로젝트에서 마이크로서비스 확장을 위해 모듈별로 분리되었으며, 각 Tool 서버는 독립적으로 확장 및 배포할 수 있습니다.)* *(참고: 기존 단일 모듈 프로젝트에서 마이크로서비스 확장을 위해 모듈별로 분리되었으며, 각 Tool 서버는 독립적으로 확장 및 배포할 수 있습니다.)*

125
add_javadoc.py Normal file
View File

@@ -0,0 +1,125 @@
import os
import re
JAVADOC_FILES = [
r'.\McpBridge.java',
r'.\dap-common\src\main\java\io\shinhanlife\dap\common\mcp\exception\GlobalExceptionHandler.java',
r'.\dap-common\src\main\java\io\shinhanlife\dap\common\util\ToolSourceUpdater.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowAppServiceId.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowControllerId.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowIndexPaging.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowMciFieldInfo.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowMybatisMapper.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowServiceGroupId.java',
r'.\dap-common\src\main\java\io\shinhanlife\glow\GlowTrgmField.java',
r'.\dap-common\src\test\java\io\shinhanlife\glow\util\GlowMciParserTest.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\DapGatewayApplication.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\audit\AuditLogService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\config\AgentResponseBudgetProperties.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\config\McpGatewayProperties.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\guardrail\GuardrailService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\guardrail\SensitiveDataMasker.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\guardrail\ToolResponseGuardrailService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\presentation\ChatController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\presentation\McpRouterController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\presentation\ScaffoldingController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\redis\McpMonitorEventService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\redis\RedisToolTraceService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreaker.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerMonitorController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerSnapshot.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\CircuitBreakerState.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\FailureType.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\RetryPolicy.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\resilience\ToolExecutionException.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\security\McpRequestContext.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\security\McpRequestContextResolver.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\security\ToolAuthorizationService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\service\ExecuteService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\sync\DynamicMcpController.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\sync\DynamicMcpServerManager.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\sync\RegistryMcpToolSpecificationFactory.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\AgentResponseBudgetService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\LargeToolResponseService.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\PaginationRequestValidator.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\result\ToolExecutionResult.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\tool\result\ToolExecutionResultFormatter.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\transport\HttpToolInvoker.java',
r'.\dap-gateway\src\main\java\io\shinhanlife\dap\biz\mcp\gateway\transport\ToolInvoker.java',
r'.\dap-gateway\src\test\java\io\shinhanlife\dap\biz\mcp\gateway\DapGatewayApplicationTests.java',
r'.\dap-gateway\src\test\java\io\shinhanlife\dap\biz\mcp\gateway\tool\large\PaginationRequestValidatorTest.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\sample\converter\SampleLegacyConverter.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\sample\dto\SampleAiReqDto.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\annotation\McpFunction.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\annotation\McpParameter.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\annotation\McpTool.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\aop\ToolSlaMonitoringAspect.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\presentation\BusinessToolController.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\service\ToolRegistryHeartbeatSender.java',
r'.\dap-tool-core\src\main\java\io\shinhanlife\dap\biz\mcp\tool\util\JsonSchemaGenerator.java',
r'.\dap-tool-core\src\test\java\io\shinhanlife\dap\biz\mcp\sample\converter\SampleLegacyConverterTest.java',
r'.\dap-tool-email\src\main\java\io\shinhanlife\dap\biz\mcp\tool\email\DapToolEmailApplication.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\dto\BalanceReq.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\dto\BalanceRes.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\other\DapToolOtherApplication.java',
r'.\dap-tool-other\src\main\java\io\shinhanlife\dap\biz\mcp\tool\service\BalanceService.java',
r'.\dap-tool-payment\src\main\java\io\shinhanlife\dap\biz\mcp\tool\payment\DapToolPaymentApplication.java',
r'.\dap-tool-sms\src\main\java\io\shinhanlife\dap\biz\mcp\tool\sms\DapToolSmsApplication.java',
r'.\dap-tool-sms\src\main\java\io\shinhanlife\dap\biz\mcp\tool\sms\converter\SmsLegacyConverter.java',
r'.\dap-tool-sms\src\main\java\io\shinhanlife\dap\biz\mcp\tool\sms\dto\SmsLegacyReqDto.java',
]
JAVADOC_TEMPLATE = '''/**
* @package {package}
* @className {classname}
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/'''
def get_package_and_class(content, filename):
pkg_match = re.search(r'^package\s+([^\s;]+)', content, re.MULTILINE)
package = pkg_match.group(1) if pkg_match else 'io.shinhanlife'
classname = os.path.splitext(filename)[0]
return package, classname
def add_javadoc(file_path):
if not os.path.exists(file_path):
print(f'SKIP (not found): {file_path}')
return
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
if '@className' in content or '@author' in content:
print(f'SKIP (already has javadoc): {file_path}')
return
filename = os.path.basename(file_path)
package, classname = get_package_and_class(content, filename)
javadoc = JAVADOC_TEMPLATE.format(package=package, classname=classname)
# Insert after package declaration
pkg_pattern = re.compile(r'^(package\s+[^\s;]+;\s*\n)', re.MULTILINE)
match = pkg_pattern.search(content)
if match:
insert_pos = match.end()
new_content = content[:insert_pos] + '\n' + javadoc + '\n' + content[insert_pos:]
else:
new_content = javadoc + '\n' + content
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f'OK: {file_path}')
for f in JAVADOC_FILES:
add_javadoc(f)
print('\nDone!')

View File

@@ -1,13 +0,0 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowAppServiceId {
String value();
String description() default "";
}

View File

@@ -1,5 +0,0 @@
package io.shinhanlife.glow;
public @interface GlowControllerId {
String value();
}

View File

@@ -1,9 +0,0 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowIndexPaging {
}

View File

@@ -1,7 +0,0 @@
package io.shinhanlife.glow;
public @interface GlowServiceGroupId {
String value();
String description();
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.gateway", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.gateway", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching
public class AxHubGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(AxHubGatewayApplication.class, args);
}
}

View File

@@ -1,12 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience;
/**
* Circuit Breaker의 현재 상태입니다.
*
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
*/
public enum CircuitBreakerState {
CLOSED,
OPEN,
HALF_OPEN
}

View File

@@ -1,87 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.gateway.sync;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* MCP 서버의 tools/list 노출 목록을 Redis Registry의 ACTIVE Tool 목록과 동기화합니다.
*/
@Service
public class RegistryMcpToolSynchronizer {
private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class);
private final ObjectProvider<McpSyncServer> mcpServerProvider;
private final RedisRegistryService redisRegistryService;
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final Set<String> managedToolNames = new LinkedHashSet<>();
private final ReentrantLock lock = new ReentrantLock();
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
RedisRegistryService redisRegistryService,
RegistryMcpToolSpecificationFactory specificationFactory) {
this.mcpServerProvider = mcpServerProvider;
this.redisRegistryService = redisRegistryService;
this.specificationFactory = specificationFactory;
}
@EventListener(ApplicationReadyEvent.class)
public void synchronizeOnStartup() {
synchronize();
}
@Scheduled(fixedDelayString = "${mcp.tool-registry-sync-interval-millis:5000}")
public void synchronizeScheduled() {
synchronize();
}
public void synchronize() {
McpSyncServer server = mcpServerProvider.getIfAvailable();
if (server == null) {
return;
}
lock.lock();
try {
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
Set<String> activeNames = activeEntries.stream()
.map(ToolMetadata::getName) // We use 'name' for MCP Tool Name
.collect(Collectors.toCollection(LinkedHashSet::new));
Set<String> removedNames = new LinkedHashSet<>(managedToolNames);
removedNames.removeAll(activeNames);
for (String removedName : removedNames) {
server.removeTool(removedName);
managedToolNames.remove(removedName);
log.info("Removed MCP registry tool. tool={}", removedName);
}
for (ToolMetadata entry : activeEntries) {
if (managedToolNames.contains(entry.getName())) {
server.removeTool(entry.getName());
}
server.addTool(specificationFactory.create(entry));
managedToolNames.add(entry.getName());
}
} catch (Exception error) {
log.warn("MCP registry tool synchronization failed. message={}", error.getMessage());
} finally {
lock.unlock();
}
}
}

View File

@@ -1,11 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.gateway.transport;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Map;
/**
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
*/
public interface ToolInvoker {
JsonNode invoke(Map<String, Object> payload, String targetUrl);
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.converter;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.RoleListRequest;
import org.mapstruct.Mapper;
/**
* @package io.shinhanlife.axhub.biz.so.atm.converter
* @className AccessMgmtConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class AccessMgmtConverter {
public abstract RoleListInDto toInDto(RoleListRequest request);
public abstract AthrSearchInDto toInDto(AthrSearchRequest request);
public abstract AthrSaveInDto toInDto(AthrSaveRequest request);
public abstract AthrSearchResponse toResponse(AthrOutDto outDto);
}

View File

@@ -1,48 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.domain.repository;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrItemOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleKnwlAthrInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleToolAthrInDto;
import io.shinhanlife.glow.GlowMybatisMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.domain.repository
* @className AccessMgmtMapper
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface AccessMgmtMapper {
List<RoleListOutDto> selectRoles(RoleListInDto inDto);
List<AthrItemOutDto> selectAllTools();
List<AthrItemOutDto> selectAllKnwls();
List<String> selectGrantedToolIds(AthrSearchInDto inDto);
List<String> selectGrantedKnwlIds(AthrSearchInDto inDto);
void deleteToolAthr(@Param("systId") String systId, @Param("roleNo") String roleNo);
void insertToolAthr(RoleToolAthrInDto inDto);
void deleteKnwlAthr(@Param("systId") String systId, @Param("roleNo") String roleNo);
void insertKnwlAthr(RoleKnwlAthrInDto inDto);
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.domain.service;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.domain.service
* @className AccessMgmtService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface AccessMgmtService {
List<RoleListOutDto> getRoles(RoleListInDto inDto);
AthrOutDto getAthr(AthrSearchInDto inDto);
void saveAthr(AthrSaveInDto inDto);
}

View File

@@ -1,142 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.domain.service.impl;
import io.shinhanlife.axhub.biz.so.atm.domain.repository.AccessMgmtMapper;
import io.shinhanlife.axhub.biz.so.atm.domain.service.AccessMgmtService;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrItemOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSaveInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrSearchInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleKnwlAthrInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListInDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleToolAthrInDto;
import io.shinhanlife.axhub.common.util.SessionUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.axhub.biz.so.atm.domain.service.impl
* @className AccessMgmtServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class AccessMgmtServiceImpl implements AccessMgmtService {
private final AccessMgmtMapper accessMgmtMapper;
private static final String SYSTEM_CD = "AXH";
private static final String SYSTEM_PRG = "SOATM0100";
@Override
public List<RoleListOutDto> getRoles(RoleListInDto inDto) {
return accessMgmtMapper.selectRoles(inDto);
}
@Override
public AthrOutDto getAthr(AthrSearchInDto inDto) {
Set<String> grantedToolIds = accessMgmtMapper.selectGrantedToolIds(inDto)
.stream().collect(Collectors.toSet());
Set<String> grantedKnwlIds = accessMgmtMapper.selectGrantedKnwlIds(inDto)
.stream().collect(Collectors.toSet());
List<AthrItemOutDto> tools = accessMgmtMapper.selectAllTools()
.stream()
.peek(item -> item.setGranted(grantedToolIds.contains(item.getResourceId())))
.collect(Collectors.toList());
List<AthrItemOutDto> knwls = accessMgmtMapper.selectAllKnwls()
.stream()
.peek(item -> item.setGranted(grantedKnwlIds.contains(item.getResourceId())))
.collect(Collectors.toList());
return new AthrOutDto(tools, knwls);
}
@Override
@Transactional
public void saveAthr(AthrSaveInDto inDto) {
Date now = new Date();
String systId = inDto.getSystId();
String roleNo = inDto.getRoleNo();
accessMgmtMapper.deleteToolAthr(systId, roleNo);
if (inDto.getGrantedToolIds() != null) {
for (String toolId : inDto.getGrantedToolIds()) {
accessMgmtMapper.insertToolAthr(buildToolAthrInDto(systId, roleNo, toolId, now));
}
}
accessMgmtMapper.deleteKnwlAthr(systId, roleNo);
if (inDto.getGrantedKnwlIds() != null) {
for (String knwlId : inDto.getGrantedKnwlIds()) {
accessMgmtMapper.insertKnwlAthr(buildKnwlAthrInDto(systId, roleNo, knwlId, now));
}
}
}
private RoleToolAthrInDto buildToolAthrInDto(String systId, String roleNo, String toolId, Date now) {
RoleToolAthrInDto dto = new RoleToolAthrInDto();
dto.setRoleToolAthrId("RTA-" + shortUuid());
dto.setSystId(systId);
dto.setRoleNo(roleNo);
dto.setToolId(toolId);
dto.setPuseYn("Y");
fillAudit(dto, now);
return dto;
}
private RoleKnwlAthrInDto buildKnwlAthrInDto(String systId, String roleNo, String knwlId, Date now) {
RoleKnwlAthrInDto dto = new RoleKnwlAthrInDto();
dto.setRoleKnwlAthrId("RKA-" + shortUuid());
dto.setSystId(systId);
dto.setRoleNo(roleNo);
dto.setKnwlId(knwlId);
dto.setPuseYn("Y");
fillAudit(dto, now);
return dto;
}
private void fillAudit(RoleToolAthrInDto dto, Date now) {
String prafNo = SessionUtil.getPrafNo();
String ognzNo = SessionUtil.getOgnzNo();
dto.setSystRgiDt(now); dto.setSystRgiPrafNo(prafNo);
dto.setSystRgiOgnzNo(ognzNo); dto.setSystRgiSystCd(SYSTEM_CD);
dto.setSystRgiPrgrId(SYSTEM_PRG);
dto.setSystChgDt(now); dto.setSystChgPrafNo(prafNo);
dto.setSystChgOgnzNo(ognzNo); dto.setSystChgSystCd(SYSTEM_CD);
dto.setSystChgPrgrId(SYSTEM_PRG);
}
private void fillAudit(RoleKnwlAthrInDto dto, Date now) {
String prafNo = SessionUtil.getPrafNo();
String ognzNo = SessionUtil.getOgnzNo();
dto.setSystRgiDt(now); dto.setSystRgiPrafNo(prafNo);
dto.setSystRgiOgnzNo(ognzNo); dto.setSystRgiSystCd(SYSTEM_CD);
dto.setSystRgiPrgrId(SYSTEM_PRG);
dto.setSystChgDt(now); dto.setSystChgPrafNo(prafNo);
dto.setSystChgOgnzNo(ognzNo); dto.setSystChgSystCd(SYSTEM_CD);
dto.setSystChgPrgrId(SYSTEM_PRG);
}
private String shortUuid() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 12).toUpperCase();
}
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.axhub.biz.so.atm.dto
* @className AthrItemOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrItemOutDto {
private String resourceId;
private String resourceNm;
private String resourceDs;
private String typeCode;
private boolean granted;
}

View File

@@ -1,36 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.dto
* @className AthrSaveInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrSaveInDto {
private String systId;
private String roleNo;
private List<String> grantedToolIds;
private List<String> grantedKnwlIds;
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.axhub.biz.so.atm.dto
* @className AthrSearchInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AthrSearchInDto {
private String systId;
private String roleNo;
}

View File

@@ -1,48 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @package io.shinhanlife.axhub.biz.so.atm.dto
* @className RoleKnwlAthrInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleKnwlAthrInDto {
private String roleKnwlAthrId;
private String systId;
private String roleNo;
private String knwlId;
private String puseYn;
private Date systRgiDt;
private String systRgiPrafNo;
private String systRgiOgnzNo;
private String systRgiSystCd;
private String systRgiPrgrId;
private Date systChgDt;
private String systChgPrafNo;
private String systChgOgnzNo;
private String systChgSystCd;
private String systChgPrgrId;
}

View File

@@ -1,33 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.axhub.biz.so.atm.dto
* @className RoleListInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleListInDto {
private String systId;
private String puseYn;
private String keyword;
}

View File

@@ -1,48 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.dto;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @package io.shinhanlife.axhub.biz.so.atm.dto
* @className RoleToolAthrInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RoleToolAthrInDto {
private String roleToolAthrId;
private String systId;
private String roleNo;
private String toolId;
private String puseYn;
private Date systRgiDt;
private String systRgiPrafNo;
private String systRgiOgnzNo;
private String systRgiSystCd;
private String systRgiPrgrId;
private Date systChgDt;
private String systChgPrafNo;
private String systChgOgnzNo;
private String systChgSystCd;
private String systChgPrgrId;
}

View File

@@ -1,59 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.presentation;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.RoleListRequest;
import io.shinhanlife.axhub.biz.so.atm.usecase.AccessMgmtUseCase;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.ResponseUtil;
import lombok.RequiredArgsConstructor;
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;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.presentation
* @className SOATM0100Controller
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequestMapping("/so/atm")
@RequiredArgsConstructor
public class SOATM0100Controller {
private final AccessMgmtUseCase accessMgmtUseCase;
@PostMapping("/SOATM0100R")
public ResponseEntity<BaseResponse<List<RoleListOutDto>>> getRoles(
@RequestBody RoleListRequest request) {
return ResponseUtil.ok(accessMgmtUseCase.getRoles(request));
}
@PostMapping("/SOATM0101R")
public ResponseEntity<BaseResponse<AthrSearchResponse>> getAthr(
@RequestBody AthrSearchRequest request) {
return ResponseUtil.ok(accessMgmtUseCase.getAthr(request));
}
@PostMapping("/SOATM0100U")
public ResponseEntity<BaseResponse<Void>> saveAthr(
@RequestBody AthrSaveRequest request) {
accessMgmtUseCase.saveAthr(request);
return ResponseUtil.ok();
}
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.presentation.io
* @className AthrSaveRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class AthrSaveRequest {
private String systId;
private String roleNo;
private List<String> grantedToolIds;
private List<String> grantedKnwlIds;
}

View File

@@ -1,28 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.presentation.io;
import io.shinhanlife.axhub.biz.so.atm.dto.AthrItemOutDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.presentation.io
* @className AthrSearchResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@AllArgsConstructor
public class AthrSearchResponse {
private List<AthrItemOutDto> tools;
private List<AthrItemOutDto> knwls;
}

View File

@@ -1,26 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.presentation.io;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.axhub.biz.so.atm.presentation.io
* @className RoleListRequest
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
public class RoleListRequest {
private String systId;
private String puseYn;
private String keyword;
}

View File

@@ -1,32 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.usecase;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.RoleListRequest;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.usecase
* @className AccessMgmtUseCase
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface AccessMgmtUseCase {
List<RoleListOutDto> getRoles(RoleListRequest request);
AthrSearchResponse getAthr(AthrSearchRequest request);
void saveAthr(AthrSaveRequest request);
}

View File

@@ -1,51 +0,0 @@
package io.shinhanlife.axhub.biz.so.atm.usecase.impl;
import io.shinhanlife.axhub.biz.so.atm.converter.AccessMgmtConverter;
import io.shinhanlife.axhub.biz.so.atm.domain.service.AccessMgmtService;
import io.shinhanlife.axhub.biz.so.atm.dto.RoleListOutDto;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSaveRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchRequest;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.AthrSearchResponse;
import io.shinhanlife.axhub.biz.so.atm.presentation.io.RoleListRequest;
import io.shinhanlife.axhub.biz.so.atm.usecase.AccessMgmtUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @package io.shinhanlife.axhub.biz.so.atm.usecase.impl
* @className AccessMgmtUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class AccessMgmtUseCaseImpl implements AccessMgmtUseCase {
private final AccessMgmtService accessMgmtService;
private final AccessMgmtConverter converter;
@Override
public List<RoleListOutDto> getRoles(RoleListRequest request) {
return accessMgmtService.getRoles(converter.toInDto(request));
}
@Override
public AthrSearchResponse getAthr(AthrSearchRequest request) {
return converter.toResponse(accessMgmtService.getAthr(converter.toInDto(request)));
}
@Override
public void saveAthr(AthrSaveRequest request) {
accessMgmtService.saveAthr(converter.toInDto(request));
}
}

View File

@@ -1,12 +0,0 @@
# Dev 환경 전용 설정 (개발 서버 DB 연결 정보 등)
# spring.datasource.url=jdbc:oracle:thin:@//dev-db-host:1521/XEPDB1
# spring.datasource.driverClassName=oracle.jdbc.OracleDriver
# spring.datasource.username=dev_user
# spring.datasource.password=dev_password
# Render 클라우드 환경 전용 Fallback 라우팅
mcp.gateway.fallback.routes.sms=https://axhub-tool-sms.onrender.com
mcp.gateway.fallback.routes.email=https://axhub-tool-email.onrender.com
mcp.gateway.fallback.default-url=https://axhub-tool-other.onrender.com
mcp.gateway.fallback.routes.payment=https://axhub-tool-payment.onrender.com
mcp.gateway.fallback.routes.hr=https://axhub-tool-hr.onrender.com

View File

@@ -1,5 +0,0 @@
# Prod 환경 전용 설정 (운영 서버 DB 연결 정보 등)
# spring.datasource.url=jdbc:oracle:thin:@//prod-db-host:1521/PROD
# spring.datasource.driverClassName=oracle.jdbc.OracleDriver
# spring.datasource.username=prod_user
# spring.datasource.password=prod_password

View File

@@ -1,12 +0,0 @@
plugins {
id 'java-library'
}
dependencies {
api project(':axhub-common')
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -1,11 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.annotation;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpParameter {
String description();
boolean required() default false;
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.email;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching
public class AxHubToolEmailApplication {
public static void main(String[] args) {
SpringApplication.run(AxHubToolEmailApplication.class, args);
}
}

View File

@@ -1,10 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.dto;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpParameter;
import lombok.Data;
@Data
public class BalanceReq {
@McpParameter(description = "고객의 계좌번호 (- 제외)", required = true)
private String accountNumber;
}

View File

@@ -1,11 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.dto;
import lombok.Data;
@Data
public class BalanceRes {
private String status;
private String message;
private String accountNumber;
private long balance;
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.other;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching
public class AxHubToolOtherApplication {
public static void main(String[] args) {
SpringApplication.run(AxHubToolOtherApplication.class, args);
}
}

View File

@@ -1,44 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.service;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
import io.shinhanlife.axhub.biz.mcp.tool.dto.VacationRegisterReq;
import io.shinhanlife.axhub.biz.mcp.tool.dto.LeaveCountReq;
import io.shinhanlife.axhub.biz.mcp.tool.dto.LeaveCountReq;
@McpTool(
routingType = "HTTP",
categoryKey = "hr"
)
/**
* @package io.shinhanlife.axhub.biz.mcp.tool.service
* @className CommonUtilityService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class CommonUtilityService extends AbstractMcpToolService {
@McpFunction(displayName = "register_vacation 툴", name = "register_vacation", description = "휴가 등록", prompt = "내일 하루 연차 휴가를 등록해줘.", mappingId = "HR_VAC_01")
public Object registerVacation(VacationRegisterReq data) {
return executeLegacy("HTTP", "HR_VAC_01", data);
}
@McpFunction(displayName = "get_leave_count 툴", name = "get_leave_count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
public Object getLeaveCount(LeaveCountReq data) {
return executeLegacy("HTTP", "HR_VAC_02", data);
}
@McpFunction(displayName = "secret_tool 툴", name = "secret_tool", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
public Object secretTool(LeaveCountReq data) {
return executeLegacy("HTTP", "SECRET_001", data);
}
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.service;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
import io.shinhanlife.axhub.biz.mcp.tool.dto.ContractStatusReq;
import io.shinhanlife.axhub.biz.mcp.tool.dto.ContractDetailReq;
@McpTool(
routingType = "HTTP",
categoryKey = "contract"
)
/**
* @package io.shinhanlife.axhub.biz.mcp.tool.service
* @className ContractInquiryService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class ContractInquiryService extends AbstractMcpToolService {
@McpFunction(displayName = "contract_status 툴", name = "contract_status", description = "계약상태 조회", prompt = "김신한 고객의 현재 계약 상태를 조회해줘.", mappingId = "CNTR_001")
public Object getStatus(ContractStatusReq data) {
return executeLegacy("HTTP", "CNTR_001", data);
}
@McpFunction(displayName = "contract_detail 툴", name = "contract_detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
public Object getDetail(ContractDetailReq data) {
return executeLegacy("HTTP", "CNTR_002", data);
}
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.service;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpFunction;
import io.shinhanlife.axhub.biz.mcp.tool.annotation.McpTool;
import io.shinhanlife.axhub.biz.mcp.tool.dto.CustomerGradeReq;
import io.shinhanlife.axhub.biz.mcp.tool.dto.CustomerDetailReq;
@McpTool(
routingType = "TCP",
categoryKey = "customer"
)
/**
* @package io.shinhanlife.axhub.biz.mcp.tool.service
* @className CustomerInfoService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class CustomerInfoService extends AbstractMcpToolService {
@McpFunction(displayName = "grade 툴", name = "grade", description = "고객등급 조회", prompt = "이 고객의 VIP 등급을 조회해줘.", mappingId = "CRM_001")
public Object getGrade(CustomerGradeReq req) {
return executeLegacy("TCP", "CRM_001", req);
}
@McpFunction(displayName = "detail 툴", name = "detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
public Object getDetail(CustomerDetailReq data) {
return executeLegacy("TCP", "CRM_002", data);
}
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.payment;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching
public class PaymentToolApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentToolApplication.class, args);
}
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.sms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.axhub.biz.mcp.tool", "io.shinhanlife.axhub.biz.mcp.adapter", "io.shinhanlife.axhub.common.mcp", "io.shinhanlife.axhub.common.config"})
@EnableCaching
public class AxHubToolSmsApplication {
public static void main(String[] args) {
SpringApplication.run(AxHubToolSmsApplication.class, args);
}
}

View File

@@ -1,15 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.sms.converter;
import io.shinhanlife.axhub.biz.mcp.tool.dto.SmsSendReq;
import io.shinhanlife.axhub.biz.mcp.tool.sms.dto.SmsLegacyReqDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring")
public interface SmsLegacyConverter {
@Mapping(source = "phoneNumber", target = "phone")
@Mapping(source = "message", target = "content")
SmsLegacyReqDto toLegacyReq(SmsSendReq req);
}

View File

@@ -1,18 +0,0 @@
package io.shinhanlife.axhub.biz.mcp.tool.sms.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class SmsLegacyReqDto {
/**
* EAI 시스템이 요구하는 수신자 번호 파라미터명
*/
private String phone;
/**
* EAI 시스템이 요구하는 메시지 내용 파라미터명
*/
private String content;
}

View File

@@ -3,15 +3,16 @@ WORKDIR /app
COPY gradlew . COPY gradlew .
COPY gradle gradle COPY gradle gradle
COPY build.gradle settings.gradle ./ COPY build.gradle settings.gradle ./
COPY axhub-common axhub-common
COPY axhub-gateway axhub-gateway COPY dap-tool-core dap-tool-core
COPY dap-gateway dap-gateway
RUN chmod +x gradlew RUN chmod +x gradlew
RUN ./gradlew :axhub-gateway:build -x test RUN ./gradlew :dap-gateway:build -x test
FROM eclipse-temurin:21-jre-alpine FROM eclipse-temurin:21-jre-alpine
WORKDIR /app WORKDIR /app
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul ENV TZ=Asia/Seoul
COPY --from=builder /app/axhub-gateway/build/libs/*-SNAPSHOT.jar app.jar COPY --from=builder /app/dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8081 EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -3,7 +3,7 @@ plugins {
} }
dependencies { dependencies {
implementation project(':axhub-common') implementation project(':dap-tool-core')
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-redis'

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.mcg;
/**
* @package io.shinhanlife.dap.mcg
* @className DapGatewayApplication
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
@EnableCaching
public class DapGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(DapGatewayApplication.class, args);
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.axhub.biz.mcp.gateway.aop; package io.shinhanlife.dap.mcg.aop;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
@@ -8,7 +8,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.aop * @package io.shinhanlife.dap.mcg.aop
* @className GatewayLoggingAspect * @className GatewayLoggingAspect
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식
@@ -27,7 +27,7 @@ import org.springframework.util.StopWatch;
public class GatewayLoggingAspect { public class GatewayLoggingAspect {
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동 // gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동
@Around("execution(* io.shinhanlife.axhub.biz.mcp.gateway.presentation..*(..))") @Around("execution(* io.shinhanlife.dap.mcg.presentation..*(..))")
public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String targetMethod = joinPoint.getSignature().toShortString(); String targetMethod = joinPoint.getSignature().toShortString();
StopWatch stopWatch = new StopWatch(); StopWatch stopWatch = new StopWatch();

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.axhub.biz.mcp.gateway.audit; package io.shinhanlife.dap.mcg.audit;
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker; /**
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContext; * @package io.shinhanlife.dap.mcg.audit
* @className AuditLogService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.config; package io.shinhanlife.dap.mcg.config;
/**
* @package io.shinhanlife.dap.mcg.config
* @className AgentResponseBudgetProperties
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.axhub.biz.mcp.gateway.config; package io.shinhanlife.dap.mcg.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -8,7 +8,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.config * @package io.shinhanlife.dap.mcg.config
* @className GatewayFallbackProperties * @className GatewayFallbackProperties
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.config; package io.shinhanlife.dap.mcg.config;
/**
* @package io.shinhanlife.dap.mcg.config
* @className McpGatewayProperties
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.axhub.biz.mcp.gateway.config; package io.shinhanlife.dap.mcg.config;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -10,7 +10,7 @@ import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.config * @package io.shinhanlife.dap.mcg.config
* @className RedisConfig * @className RedisConfig
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.axhub.biz.mcp.gateway.dto; package io.shinhanlife.dap.mcg.dto;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.dto * @package io.shinhanlife.dap.mcg.dto
* @className OperationType * @className OperationType
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.axhub.biz.mcp.gateway.dto; package io.shinhanlife.dap.mcg.dto;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@@ -17,7 +17,7 @@ import java.util.HashSet;
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan) 사용됩니다. * Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan) 사용됩니다.
*/ */
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.dto * @package io.shinhanlife.dap.mcg.dto
* @className ToolMetadata * @className ToolMetadata
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식
@@ -71,6 +71,18 @@ public class ToolMetadata {
@Builder.Default @Builder.Default
private Boolean requiresApproval = false; private Boolean requiresApproval = false;
@Builder.Default
private Boolean readOnlyHint = false;
@Builder.Default
private Boolean destructiveHint = false;
@Builder.Default
private Boolean idempotentHint = false;
@Builder.Default
private Boolean openWorldHint = false;
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI) // 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.axhub.biz.mcp.gateway.guardrail; package io.shinhanlife.dap.mcg.guardrail;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException; /**
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; * @package io.shinhanlife.dap.mcg.guardrail
* @className GuardrailService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.guardrail; package io.shinhanlife.dap.mcg.guardrail;
/**
* @package io.shinhanlife.dap.mcg.guardrail
* @className SensitiveDataMasker
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.axhub.biz.mcp.gateway.guardrail; package io.shinhanlife.dap.mcg.guardrail;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException; /**
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; * @package io.shinhanlife.dap.mcg.guardrail
* @className ToolResponseGuardrailService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.axhub.biz.mcp.gateway.messaging; package io.shinhanlife.dap.mcg.messaging;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -10,7 +10,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.messaging * @package io.shinhanlife.dap.mcg.messaging
* @className KafkaProducerService * @className KafkaProducerService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.axhub.biz.mcp.gateway.presentation; package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService; /**
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService; * @package io.shinhanlife.dap.mcg.presentation
* @className ChatController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
@@ -28,7 +43,10 @@ public class ChatController {
private final ChatClient.Builder chatClientBuilder; private final ChatClient.Builder chatClientBuilder;
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request) { public SseEmitter chatStream(@RequestBody Map<String, String> request,
@RequestHeader(value = "X-Agent-Id", required = false) String agentId,
@RequestHeader(value = "X-Tenant-Id", required = false, defaultValue = "system") String tenantId) {
String effectiveTenantId = (agentId != null && !agentId.trim().isEmpty()) ? agentId : tenantId;
String message = request.getOrDefault("message", "").trim(); String message = request.getOrDefault("message", "").trim();
log.info("[Real AI Chat] 사용자의 메시지 수신: {}", message); log.info("[Real AI Chat] 사용자의 메시지 수신: {}", message);
@@ -38,7 +56,7 @@ public class ChatController {
List<ToolCallback> callbacks = new ArrayList<>(); List<ToolCallback> callbacks = new ArrayList<>();
for (ToolMetadata meta : registryService.getAllTools()) { for (ToolMetadata meta : registryService.getAllTools()) {
if (Boolean.TRUE.equals(meta.getVisible())) { if (Boolean.TRUE.equals(meta.getVisible())) {
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper)); callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
} }
} }
@@ -100,11 +118,13 @@ public class ChatController {
private final ExecuteService executeService; private final ExecuteService executeService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final ToolDefinition toolDefinition; private final ToolDefinition toolDefinition;
private final String tenantId;
public DynamicMcpToolCallback(ToolMetadata metadata, ExecuteService executeService, ObjectMapper objectMapper) { public DynamicMcpToolCallback(ToolMetadata metadata, ExecuteService executeService, ObjectMapper objectMapper, String tenantId) {
this.metadata = metadata; this.metadata = metadata;
this.executeService = executeService; this.executeService = executeService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.tenantId = tenantId;
String schema = "{\"type\":\"object\",\"properties\":{}}"; String schema = "{\"type\":\"object\",\"properties\":{}}";
try { try {
@@ -148,7 +168,7 @@ public class ChatController {
payload.put("params", params); payload.put("params", params);
Object result = executeService.execute(payload, "default"); Object result = executeService.execute(payload, this.tenantId);
String jsonResult = objectMapper.writeValueAsString(result); String jsonResult = objectMapper.writeValueAsString(result);
log.info("[Function Calling] '{}' 툴 실행 완료. 결과: {}", metadata.getName(), jsonResult); log.info("[Function Calling] '{}' 툴 실행 완료. 결과: {}", metadata.getName(), jsonResult);
return jsonResult; return jsonResult;

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.axhub.biz.mcp.gateway.presentation; package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.axhub.biz.mcp.gateway.service.KillSwitchService; import io.shinhanlife.dap.mcg.service.KillSwitchService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.presentation * @package io.shinhanlife.dap.mcg.presentation
* @className KillSwitchController * @className KillSwitchController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,14 +1,29 @@
package io.shinhanlife.axhub.biz.mcp.gateway.presentation; package io.shinhanlife.dap.mcg.presentation;
/**
* @package io.shinhanlife.dap.mcg.presentation
* @className McpRouterController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcRequest; import io.shinhanlife.dap.common.adapter.dto.JsonRpcRequest;
import io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcResponse; import io.shinhanlife.dap.common.adapter.dto.JsonRpcResponse;
import io.shinhanlife.axhub.biz.mcp.adapter.dto.Params; import io.shinhanlife.dap.common.adapter.dto.Params;
import io.shinhanlife.axhub.biz.mcp.gateway.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService; import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.axhub.common.mcp.security.SecurityProperties; import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -76,7 +91,7 @@ public class McpRouterController {
try { try {
Object result = executeService.execute(payload, effectiveTenantId); Object result = executeService.execute(payload, effectiveTenantId);
io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcResponse response = new io.shinhanlife.axhub.biz.mcp.adapter.dto.JsonRpcResponse(); io.shinhanlife.dap.common.adapter.dto.JsonRpcResponse response = new io.shinhanlife.dap.common.adapter.dto.JsonRpcResponse();
response.setJsonrpc("2.0"); response.setJsonrpc("2.0");
response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString()); response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString());
response.setResult(result); response.setResult(result);

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.axhub.biz.mcp.gateway.presentation; package io.shinhanlife.dap.mcg.presentation;
import io.shinhanlife.axhub.common.util.PodScaffolder;
import io.shinhanlife.axhub.common.util.ToolScaffolder; /**
import io.shinhanlife.axhub.common.util.ToolSourceUpdater; * @package io.shinhanlife.dap.mcg.presentation
* @className ScaffoldingController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.common.util.PodScaffolder;
import io.shinhanlife.dap.common.util.ToolScaffolder;
import io.shinhanlife.dap.common.util.ToolSourceUpdater;
import java.io.File; import java.io.File;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -19,10 +34,10 @@ public class ScaffoldingController {
@PostMapping("/pod") @PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) { public String scaffoldPod(@RequestBody Map<String, String> req) {
try { try {
String moduleName = req.getOrDefault("moduleName", "axhub-tool-other"); String moduleName = req.getOrDefault("moduleName", "dap-tool-other");
if (!moduleName.startsWith("axhub-tool-")) moduleName = "axhub-tool-" + moduleName; if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName;
String port = req.getOrDefault("port", "8085"); String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("axhub-tool-", "").replace("-", ""); String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
String author = req.get("author"); String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name"); if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date"); String date = req.get("date");
@@ -42,7 +57,7 @@ public class ScaffoldingController {
String description = req.get("description"); String description = req.get("description");
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON")); String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
String routingType = req.getOrDefault("routingType", "HTTP"); String routingType = req.getOrDefault("routingType", "HTTP");
String moduleName = req.getOrDefault("moduleName", "axhub-tool-other"); String moduleName = req.getOrDefault("moduleName", "dap-tool-other");
String author = req.get("author"); String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name"); if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date"); String date = req.get("date");
@@ -78,13 +93,13 @@ public class ScaffoldingController {
if (sourceDir == null) sourceDir = System.getProperty("user.dir"); if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir); File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("axhub-tool-") && !f.getName().equals("axhub-tool-core")); File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-tool-core"));
if (files == null) return List.of("axhub-tool-other"); if (files == null) return List.of("dap-tool-other");
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList()); return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) { } catch (Exception e) {
return List.of("axhub-tool-other"); return List.of("dap-tool-other");
} }
} }
} }

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.redis; package io.shinhanlife.dap.mcg.redis;
/**
* @package io.shinhanlife.dap.mcg.redis
* @className McpMonitorEventService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

View File

@@ -1,9 +1,24 @@
package io.shinhanlife.axhub.biz.mcp.gateway.redis; package io.shinhanlife.dap.mcg.redis;
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker; /**
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContext; * @package io.shinhanlife.dap.mcg.redis
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; * @className RedisToolTraceService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.axhub.biz.mcp.gateway.registry; package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -13,7 +13,7 @@ import java.util.stream.Collectors;
import java.util.Objects; import java.util.Objects;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.registry * @package io.shinhanlife.dap.mcg.registry
* @className RedisRegistryService * @className RedisRegistryService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreaker
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerMonitorController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -1,6 +1,21 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Clock; import java.time.Clock;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerSnapshot
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
/** /**
* MCP Monitor 화면에 내려주는 Circuit Breaker 상태 정보입니다. * MCP Monitor 화면에 내려주는 Circuit Breaker 상태 정보입니다.
*/ */

View File

@@ -0,0 +1,27 @@
package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className CircuitBreakerState
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
/**
* Circuit Breaker의 현재 상태입니다.
*
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
*/
public enum CircuitBreakerState {
CLOSED,
OPEN,
HALF_OPEN
}

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className FailureType
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
/** /**
* Tool/EIMS/Tool 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다. * Tool/EIMS/Tool 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다.
* *

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className RetryPolicy
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
/** /**
* Tool 호출 재시도 정책입니다. * Tool 호출 재시도 정책입니다.
* *

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.resilience; package io.shinhanlife.dap.mcg.resilience;
/**
* @package io.shinhanlife.dap.mcg.resilience
* @className ToolExecutionException
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
/** /**
* Tool 실행 발생한 오류를 FailureType과 함께 전달하는 공통 예외입니다. * Tool 실행 발생한 오류를 FailureType과 함께 전달하는 공통 예외입니다.
* *

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.security; package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dap.mcg.security
* @className McpRequestContext
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import java.util.Set; import java.util.Set;
/** /**

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.security; package io.shinhanlife.dap.mcg.security;
/**
* @package io.shinhanlife.dap.mcg.security
* @className McpRequestContextResolver
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;

View File

@@ -1,10 +1,25 @@
package io.shinhanlife.axhub.biz.mcp.gateway.security; package io.shinhanlife.dap.mcg.security;
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType; /**
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException; * @package io.shinhanlife.dap.mcg.security
import io.shinhanlife.axhub.biz.mcp.gateway.dto.OperationType; * @className ToolAuthorizationService
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; * @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.OperationType;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Set; import java.util.Set;

View File

@@ -1,20 +1,37 @@
package io.shinhanlife.axhub.biz.mcp.gateway.service; package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType; /**
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.RetryPolicy; * @package io.shinhanlife.dap.mcg.service
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException; * @className ExecuteService
import io.shinhanlife.axhub.biz.mcp.gateway.dto.OperationType; * @description AX HUB 시스템 처리 클래스
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties; * @author 김형식
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker; * @create 2026.09.01
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.GuardrailService; * <pre>
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContext; * ---------- 개정이력 ----------
import io.shinhanlife.axhub.biz.mcp.gateway.security.McpRequestContextResolver; * 수정일 수정자 수정내용
import io.shinhanlife.axhub.biz.mcp.gateway.audit.AuditLogService; * ---------- -------- ---------------------------
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.CircuitBreaker; * 2026.09.01 김형식 최초생성
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.CircuitBreakerService; *
import io.shinhanlife.axhub.biz.mcp.gateway.security.ToolAuthorizationService; * </pre>
import io.shinhanlife.axhub.biz.mcp.gateway.redis.RedisToolTraceService; */
import java.util.HashMap;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.RetryPolicy;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.mcg.dto.OperationType;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.guardrail.GuardrailService;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.mcg.security.McpRequestContextResolver;
import io.shinhanlife.dap.mcg.audit.AuditLogService;
import io.shinhanlife.dap.mcg.resilience.CircuitBreaker;
import io.shinhanlife.dap.mcg.resilience.CircuitBreakerService;
import io.shinhanlife.dap.mcg.security.ToolAuthorizationService;
import io.shinhanlife.dap.mcg.redis.RedisToolTraceService;
import jakarta.annotation.PreDestroy; import jakarta.annotation.PreDestroy;
import java.util.Map; import java.util.Map;
@@ -27,12 +44,12 @@ import org.springframework.web.client.RestClient;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.shinhanlife.axhub.biz.mcp.gateway.tool.large.LargeToolResponseService; import io.shinhanlife.dap.mcg.tool.large.LargeToolResponseService;
import io.shinhanlife.axhub.biz.mcp.gateway.tool.large.PaginationRequestValidator; import io.shinhanlife.dap.mcg.tool.large.PaginationRequestValidator;
import io.shinhanlife.axhub.biz.mcp.gateway.tool.result.ToolExecutionResultFormatter; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.axhub.biz.mcp.gateway.tool.result.ToolExecutionResult; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.ToolResponseGuardrailService; import io.shinhanlife.dap.mcg.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.axhub.biz.mcp.gateway.transport.ToolInvoker; import io.shinhanlife.dap.mcg.transport.ToolInvoker;
@Slf4j @Slf4j
@Service @Service
@@ -145,22 +162,53 @@ public class ExecuteService {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
String responseText = ""; String responseText = "";
try { responseText = objectMapper.writeValueAsString(result); } catch (Exception ignore) {} long originalSize = 0;
try {
responseText = objectMapper.writeValueAsString(result);
originalSize = responseText.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
} catch (Exception ignore) {}
Map<String, Object> finalResult = new java.util.LinkedHashMap<>();
finalResult.put("status", "ok");
finalResult.put("result", result);
finalResult.put("error_code", null);
finalResult.put("error_message", null);
finalResult.put("elapsed_ms", elapsedMillis);
finalResult.put("truncated", false);
finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, ""); auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return result; return finalResult;
} catch (ToolExecutionException error) { } catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), ""); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
throw error;
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", error.failureType().name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
} catch (Exception error) { } catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), ""); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + toolName, error);
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", FailureType.INTERNAL_ERROR.name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
} }
} }
@@ -213,20 +261,17 @@ public class ExecuteService {
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) { if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl(); targetUrl = metadata.getPodUrl();
} }
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call"; String executeApiUrl = targetUrl + "/mcp/" + metadata.getName();
Map<String, String> headers = new java.util.HashMap<>();
headers.put("trace-id", context.requestId());
headers.put("request-id", java.util.UUID.randomUUID().toString());
ObjectNode pageArguments = paginationValidator.normalize(arguments); ObjectNode pageArguments = paginationValidator.normalize(arguments);
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId()); LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) { while (true) {
Map<String, Object> pagePayload = new java.util.HashMap<>(payload); Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
if (pagePayload.containsKey("params")) {
Map<String, Object> params = new java.util.HashMap<>((Map<String, Object>) pagePayload.get("params"));
params.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
pagePayload.put("params", params);
} else {
pagePayload.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
}
try { try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload)))); log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
@@ -234,13 +279,16 @@ public class ExecuteService {
JsonNode data = null; JsonNode data = null;
try { try {
data = toolInvoker.invoke(pagePayload, executeApiUrl); data = toolInvoker.invoke(pagePayload, executeApiUrl, headers);
} catch (org.springframework.web.client.RestClientResponseException e) {
// HTTP 4xx, 5xx 에러는 연결 오류가 아니라 비즈니스 로직 오류이거나 검증 실패이므로 원본 에러를 그대로 반환
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 에러 (" + e.getStatusCode() + "): " + e.getResponseBodyAsString());
} catch (Exception e) { } catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) { if (executeApiUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost"); String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl); log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try { try {
data = toolInvoker.invoke(pagePayload, fallbackUrl); data = toolInvoker.invoke(pagePayload, fallbackUrl, headers);
} catch (Exception ex) { } catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage()); throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
} }

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.axhub.biz.mcp.gateway.service; package io.shinhanlife.dap.mcg.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -6,7 +6,7 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.service * @package io.shinhanlife.dap.mcg.service
* @className KillSwitchService * @className KillSwitchService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -1,9 +1,9 @@
package io.shinhanlife.axhub.biz.mcp.gateway.service; package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.axhub.common.mcp.security.SecurityProperties; import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -12,7 +12,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.axhub.biz.mcp.gateway.service * @package io.shinhanlife.dap.mcg.service
* @className ToolPlanner * @className ToolPlanner
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 김형식

View File

@@ -0,0 +1,248 @@
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className CustomWebMvcSseServerTransportProvider
* @description AX HUB MCP Gateway SSE 전송 제공자 - SSE 기반의 MCP 서버 트랜스포트를 구현하는 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import static org.springframework.web.servlet.function.RequestPredicates.GET;
import static org.springframework.web.servlet.function.RequestPredicates.POST;
import static org.springframework.web.servlet.function.RequestPredicates.accept;
@Slf4j
public class CustomWebMvcSseServerTransportProvider implements McpServerTransportProvider {
private McpServerSession.Factory sessionFactory;
private final String sseEndpoint;
private final String messageEndpoint;
private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>();
private final Map<String, CustomMcpSessionTransport> customTransports = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper;
public CustomWebMvcSseServerTransportProvider(String sseEndpoint, String messageEndpoint, ObjectMapper objectMapper) {
this.sseEndpoint = sseEndpoint;
this.messageEndpoint = messageEndpoint;
this.objectMapper = objectMapper != null ? objectMapper : new ObjectMapper();
}
@Override
public void setSessionFactory(McpServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
sessions.values().forEach(session -> {
try {
session.closeGracefully().subscribe();
} catch (Exception ignored) {}
});
sessions.clear();
customTransports.clear();
});
}
@Override
public Mono<Void> notifyClients(String method, Object params) {
return Mono.when(sessions.values().stream()
.map(session -> session.sendNotification(method, params))
.toList());
}
public SseEmitter handleSse() {
if (sessionFactory == null) {
throw new IllegalStateException("SessionFactory not configured");
}
SseEmitter emitter = new SseEmitter(-1L);
String sessionId = UUID.randomUUID().toString();
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
emitter.onCompletion(() -> sessions.remove(sessionId));
emitter.onTimeout(() -> sessions.remove(sessionId));
new Thread(() -> {
try {
Thread.sleep(100);
emitter.send(SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
public org.springframework.web.servlet.mvc.method.annotation.SseEmitter handleCustomSse(String sessionId, String body) {
if (sessionFactory == null) {
throw new IllegalStateException("SessionFactory not configured");
}
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = new org.springframework.web.servlet.mvc.method.annotation.SseEmitter(-1L);
boolean isNew = !sessions.containsKey(sessionId);
if (isNew) {
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
customTransports.put(sessionId, sessionTransport);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
// 주의: 클라이언트가 단일 POST 응답 후 연결을 끊더라도,
// 웜 풀(Warm Pool) 스펙상 세션은 살려둬야 하므로 세션 삭제 로직 제외
} else {
// 기존 세션인 경우 Emitter 파이프만 덮어씌움 (Switching)
CustomMcpSessionTransport sessionTransport = customTransports.get(sessionId);
if (sessionTransport != null) {
sessionTransport.setEmitter(emitter);
}
}
new Thread(() -> {
try {
// Body로 들어온 메시지 즉시 비동기 처리
if (body != null && !body.trim().isEmpty()) {
handleMessage(sessionId, body, emitter);
}
} catch (Exception e) {
emitter.completeWithError(e);
}
}).start();
return emitter;
}
public org.springframework.http.ResponseEntity<String> handleMessage(String sessionId, String body) {
return handleMessage(sessionId, body, null);
}
public org.springframework.http.ResponseEntity<String> handleMessage(String sessionId, String body, org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter) {
log.info("Received POST message for sessionId: " + sessionId + ", body: " + body);
if (sessionId == null || !sessions.containsKey(sessionId)) {
return ResponseEntity.badRequest().body("Unexpected request body");
}
McpServerSession session = sessions.get(sessionId);
try {
Map<String, Object> map = objectMapper.readValue(body, new TypeReference<Map<String, Object>>() {});
JSONRPCMessage message;
if (map.containsKey("id")) {
if (map.containsKey("method")) {
message = objectMapper.convertValue(map, JSONRPCRequest.class);
} else {
message = objectMapper.convertValue(map, JSONRPCResponse.class);
}
} else {
message = objectMapper.convertValue(map, JSONRPCNotification.class);
}
log.info("Converted message type: " + message.getClass().getName());
session.handle(message).subscribe();
log.info("Message sent to session handler");
if (emitter != null && !map.containsKey("id")) {
emitter.complete();
log.info("Completed emitter for notification");
}
return ResponseEntity.ok().build();
} catch (Exception e) {
log.error("Failed to handle message", e);
return ResponseEntity.status(500).body(e.getMessage());
}
}
public boolean hasSession(String sessionId) {
return sessionId != null && sessions.containsKey(sessionId);
}
private class CustomMcpSessionTransport implements McpServerTransport {
private volatile org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter;
private final String sessionId;
public CustomMcpSessionTransport(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter, String sessionId) {
this.emitter = emitter;
this.sessionId = sessionId;
}
public void setEmitter(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter) {
this.emitter = emitter;
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> {
log.info("Sending message to SSE stream: " + message.getClass().getName());
try {
String json = objectMapper.writeValueAsString(message);
log.info("Serialized message: " + json);
if (this.emitter != null) {
this.emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json));
log.info("Message successfully sent to SSE emitter");
// Custom 프로토콜: 1회 요청당 1응답 후 종료 (스트림을 닫아버림)
// 클라이언트가 한 번의 POST 후 응답을 받고 연결을 끊기 때문
this.emitter.complete();
}
} catch (Exception e) {
log.error("Error sending message to SSE emitter", e);
if (this.emitter != null) {
this.emitter.completeWithError(e);
}
}
});
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> {
if (this.emitter != null) {
this.emitter.complete();
}
});
}
@Override
public <T> T unmarshalFrom(Object object, TypeRef<T> typeRef) {
return objectMapper.convertValue(object, objectMapper.constructType(typeRef.getType()));
}
}
}

View File

@@ -0,0 +1,87 @@
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className DynamicMcpController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import java.util.UUID;
import org.springframework.http.ResponseEntity;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
@RestController
public class DynamicMcpController {
private final DynamicMcpServerManager manager;
public DynamicMcpController(DynamicMcpServerManager manager) {
this.manager = manager;
}
@GetMapping("/mcp/sse/{category}")
public SseEmitter handleSse(@PathVariable("category") String category) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
if (transport == null) {
throw new IllegalArgumentException("Unknown category: " + category);
}
return transport.handleSse();
}
@PostMapping("/mcp/message/{category}")
public ResponseEntity<String> handleMessage(
@PathVariable("category") String category,
@RequestParam("sessionId") String sessionId,
@RequestBody String body) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
if (transport == null) {
return ResponseEntity.badRequest().body("Unknown category: " + category);
}
return transport.handleMessage(sessionId, body);
}
@PostMapping("/mcp/custom/{category}")
public ResponseEntity<?> handleCustomMcp(
@PathVariable("category") String category,
@RequestHeader(value = "Mcp-Session-Id", required = false) String sessionId,
@RequestBody(required = false) String body) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
if (transport == null) {
return ResponseEntity.badRequest().body("Unknown category: " + category);
}
boolean isNew = (sessionId == null || sessionId.isEmpty());
String activeSessionId = isNew ? java.util.UUID.randomUUID().toString() : sessionId;
if (!isNew && !transport.hasSession(activeSessionId)) {
// 클라이언트가 보낸 세션 ID가 만료되었거나 존재하지 않는 경우 (Warm Pool 스펙: 404 Not Found 반환)
return ResponseEntity.notFound().build();
}
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = transport.handleCustomSse(activeSessionId, body);
return ResponseEntity.ok()
.header("Mcp-Session-Id", activeSessionId)
.body(emitter);
}
}

View File

@@ -0,0 +1,120 @@
package io.shinhanlife.dap.mcg.sync;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className DynamicMcpServerManager
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Component
public class DynamicMcpServerManager {
private static final Logger log = LoggerFactory.getLogger(DynamicMcpServerManager.class);
private final Map<String, McpSyncServer> categoryServers = new ConcurrentHashMap<>();
private final Map<String, CustomWebMvcSseServerTransportProvider> categoryTransports = new ConcurrentHashMap<>();
private final Map<String, Set<String>> managedToolNamesPerCategory = new ConcurrentHashMap<>();
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final ObjectMapper objectMapper;
public DynamicMcpServerManager(RegistryMcpToolSpecificationFactory specificationFactory, ObjectMapper objectMapper) {
this.specificationFactory = specificationFactory;
this.objectMapper = objectMapper;
// Pre-initialize basic categories so their endpoints are always open
getOrCreateServer("common");
getOrCreateServer("hr");
getOrCreateServer("payment");
getOrCreateServer("sms");
getOrCreateServer("email");
getOrCreateServer("other");
getOrCreateServer("sample");
}
private McpSyncServer getOrCreateServer(String categoryKey) {
String safeCategory = (categoryKey == null || categoryKey.trim().isEmpty()) ? "common" : categoryKey.toLowerCase();
return categoryServers.computeIfAbsent(safeCategory, key -> {
log.info("Creating dynamic MCP Server for category: {}", key);
String ssePath = "/mcp/sse/" + key;
String msgPath = "/mcp/message/" + key;
CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper);
McpSyncServer newServer = McpServer.sync(transport)
.serverInfo("DAP-Gateway-" + key, "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
categoryTransports.put(key, transport);
managedToolNamesPerCategory.put(key, ConcurrentHashMap.newKeySet());
return newServer;
});
}
public void synchronizeCategory(String categoryKey, List<ToolMetadata> tools) {
String safeCategory = (categoryKey == null || categoryKey.trim().isEmpty()) ? "common" : categoryKey.toLowerCase();
McpSyncServer server = getOrCreateServer(safeCategory);
Set<String> managedToolNames = managedToolNamesPerCategory.get(safeCategory);
Set<String> activeNames = tools.stream()
.map(ToolMetadata::getName)
.collect(Collectors.toSet());
Set<String> removedNames = managedToolNames.stream()
.filter(name -> !activeNames.contains(name))
.collect(Collectors.toSet());
for (String removedName : removedNames) {
server.removeTool(removedName);
managedToolNames.remove(removedName);
log.info("[{}] Removed tool: {}", safeCategory, removedName);
}
for (ToolMetadata entry : tools) {
if (!managedToolNames.contains(entry.getName())) {
server.addTool(specificationFactory.create(entry));
managedToolNames.add(entry.getName());
log.info("[{}] Added tool: {}", safeCategory, entry.getName());
} else {
server.removeTool(entry.getName());
server.addTool(specificationFactory.create(entry));
}
}
}
public Set<String> getKnownCategories() {
return Collections.unmodifiableSet(categoryServers.keySet());
}
public CustomWebMvcSseServerTransportProvider getTransport(String categoryKey) {
String safeCategory = (categoryKey == null || categoryKey.trim().isEmpty()) ? "common" : categoryKey.toLowerCase();
return categoryTransports.get(safeCategory);
}
}

View File

@@ -1,10 +1,25 @@
package io.shinhanlife.axhub.biz.mcp.gateway.sync; package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.axhub.biz.mcp.gateway.dto.ToolMetadata;
import io.shinhanlife.axhub.biz.mcp.gateway.service.ExecuteService; /**
* @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSpecificationFactory
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.axhub.biz.mcp.gateway.tool.result.ToolExecutionResult; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -37,6 +52,12 @@ public class RegistryMcpToolSpecificationFactory {
.name(entry.getName()) .name(entry.getName())
.description(description(entry)) .description(description(entry))
.inputSchema(inputSchema(entry)) .inputSchema(inputSchema(entry))
.annotations(McpSchema.ToolAnnotations.builder()
.readOnlyHint(Boolean.TRUE.equals(entry.getReadOnlyHint()))
.destructiveHint(Boolean.TRUE.equals(entry.getDestructiveHint()))
.idempotentHint(Boolean.TRUE.equals(entry.getIdempotentHint()))
.openWorldHint(Boolean.TRUE.equals(entry.getOpenWorldHint()))
.build())
.build(); .build();
return McpServerFeatures.SyncToolSpecification.builder() return McpServerFeatures.SyncToolSpecification.builder()

View File

@@ -0,0 +1,114 @@
package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dap.mcg.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
/**
* @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSynchronizer
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
public class RegistryMcpToolSynchronizer {
private static final Logger log = LoggerFactory.getLogger(RegistryMcpToolSynchronizer.class);
private final ObjectProvider<McpSyncServer> mcpServerProvider;
private final DynamicMcpServerManager dynamicMcpServerManager;
private final RedisRegistryService redisRegistryService;
private final RegistryMcpToolSpecificationFactory specificationFactory;
private final Set<String> managedToolNames = new LinkedHashSet<>();
private final ReentrantLock lock = new ReentrantLock();
public RegistryMcpToolSynchronizer(ObjectProvider<McpSyncServer> mcpServerProvider,
DynamicMcpServerManager dynamicMcpServerManager,
RedisRegistryService redisRegistryService,
RegistryMcpToolSpecificationFactory specificationFactory) {
this.mcpServerProvider = mcpServerProvider;
this.dynamicMcpServerManager = dynamicMcpServerManager;
this.redisRegistryService = redisRegistryService;
this.specificationFactory = specificationFactory;
}
@EventListener(ApplicationReadyEvent.class)
public void synchronizeOnStartup() {
synchronize();
}
@Scheduled(fixedDelayString = "${mcp.tool-registry-sync-interval-millis:5000}")
public void synchronizeScheduled() {
synchronize();
}
public void synchronize() {
lock.lock();
try {
List<ToolMetadata> activeEntries = redisRegistryService.getAllTools()
.stream().filter(ToolMetadata::getVisible).collect(Collectors.toList());
// 1. Dynamic MCP Server 동기화 (카테고리별)
Map<String, List<ToolMetadata>> toolsByCategory = activeEntries.stream()
.collect(Collectors.groupingBy(
t -> (t.getCategoryKey() == null || t.getCategoryKey().trim().isEmpty()) ? "common" : t.getCategoryKey()
));
Set<String> allKnownCategories = new LinkedHashSet<>(dynamicMcpServerManager.getKnownCategories());
allKnownCategories.addAll(toolsByCategory.keySet());
for (String category : allKnownCategories) {
dynamicMcpServerManager.synchronizeCategory(category, toolsByCategory.getOrDefault(category, List.of()));
}
// 2. Global MCP Server 동기화 (기존 하위 호환)
McpSyncServer server = mcpServerProvider.getIfAvailable();
if (server != null) {
Set<String> activeNames = activeEntries.stream()
.map(ToolMetadata::getName)
.collect(Collectors.toCollection(LinkedHashSet::new));
Set<String> removedNames = new LinkedHashSet<>(managedToolNames);
removedNames.removeAll(activeNames);
for (String removedName : removedNames) {
server.removeTool(removedName);
managedToolNames.remove(removedName);
log.info("Removed MCP registry tool (Global). tool={}", removedName);
}
for (ToolMetadata entry : activeEntries) {
if (managedToolNames.contains(entry.getName())) {
server.removeTool(entry.getName());
}
server.addTool(specificationFactory.create(entry));
managedToolNames.add(entry.getName());
}
}
} catch (Exception error) {
log.warn("MCP registry tool synchronization failed. message={}", error.getMessage());
} finally {
lock.unlock();
}
}
}

View File

@@ -1,7 +1,25 @@
package io.shinhanlife.axhub.biz.mcp.gateway.tool.large; package io.shinhanlife.dap.mcg.tool.large;
import io.shinhanlife.axhub.biz.mcp.gateway.config.AgentResponseBudgetProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker; /**
* @package io.shinhanlife.dap.mcg.tool.large
* @className AgentResponseBudgetService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import java.util.Iterator;
import java.util.Map;
import io.shinhanlife.dap.mcg.config.AgentResponseBudgetProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -99,9 +117,9 @@ public class AgentResponseBudgetService {
int copied = 0; int copied = 0;
int originalFields = 0; int originalFields = 0;
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = masked.fields(); Iterator<Map.Entry<String, JsonNode>> fields = masked.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next(); Map.Entry<String, JsonNode> entry = fields.next();
originalFields++; originalFields++;
if (copied >= properties.maxFieldsPerItem()) { if (copied >= properties.maxFieldsPerItem()) {
continue; continue;
@@ -135,9 +153,9 @@ public class AgentResponseBudgetService {
ObjectNode source = (ObjectNode) item; ObjectNode source = (ObjectNode) item;
ObjectNode limited = json.createObjectNode(); ObjectNode limited = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields(); Iterator<Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next(); Map.Entry<String, JsonNode> entry = fields.next();
JsonNode value = entry.getValue(); JsonNode value = entry.getValue();
if (value.isTextual()) { if (value.isTextual()) {
limited.put(entry.getKey(), truncateText(value.asText())); limited.put(entry.getKey(), truncateText(value.asText()));
@@ -164,7 +182,7 @@ public class AgentResponseBudgetService {
fallback.put("maxItemBytes", properties.maxItemBytes()); fallback.put("maxItemBytes", properties.maxItemBytes());
if (item.isObject()) { if (item.isObject()) {
ArrayNode fieldNames = json.createArrayNode(); ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = item.fieldNames(); Iterator<String> fieldNamesIter = item.fieldNames();
while (fieldNamesIter.hasNext()) { while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next()); fieldNames.add(fieldNamesIter.next());
} }
@@ -188,9 +206,9 @@ public class AgentResponseBudgetService {
private ObjectNode copyWithout(ObjectNode source, String... excludedFields) { private ObjectNode copyWithout(ObjectNode source, String... excludedFields) {
ObjectNode copy = json.createObjectNode(); ObjectNode copy = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields(); Iterator<Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next(); Map.Entry<String, JsonNode> entry = fields.next();
if (!excluded(entry.getKey(), excludedFields)) { if (!excluded(entry.getKey(), excludedFields)) {
copy.set(entry.getKey(), entry.getValue()); copy.set(entry.getKey(), entry.getValue());
} }

View File

@@ -1,9 +1,26 @@
package io.shinhanlife.axhub.biz.mcp.gateway.tool.large; package io.shinhanlife.dap.mcg.tool.large;
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.guardrail.SensitiveDataMasker; /**
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType; * @package io.shinhanlife.dap.mcg.tool.large
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException; * @className LargeToolResponseService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import java.util.Iterator;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.guardrail.SensitiveDataMasker;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -266,7 +283,7 @@ public class LargeToolResponseService {
preview.put("maxItemBytes", properties.largeResponseMaxItemBytes()); preview.put("maxItemBytes", properties.largeResponseMaxItemBytes());
if (masked.isObject()) { if (masked.isObject()) {
ArrayNode fieldNames = json.createArrayNode(); ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = masked.fieldNames(); Iterator<String> fieldNamesIter = masked.fieldNames();
while (fieldNamesIter.hasNext()) { while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next()); fieldNames.add(fieldNamesIter.next());
} }
@@ -309,7 +326,7 @@ public class LargeToolResponseService {
private int count(JsonNode node) { private int count(JsonNode node) {
int count = 0; int count = 0;
java.util.Iterator<JsonNode> iter = node.elements(); Iterator<JsonNode> iter = node.elements();
while(iter.hasNext()) { while(iter.hasNext()) {
iter.next(); iter.next();
count++; count++;

View File

@@ -1,8 +1,23 @@
package io.shinhanlife.axhub.biz.mcp.gateway.tool.large; package io.shinhanlife.dap.mcg.tool.large;
import io.shinhanlife.axhub.biz.mcp.gateway.config.McpGatewayProperties;
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.FailureType; /**
import io.shinhanlife.axhub.biz.mcp.gateway.resilience.ToolExecutionException; * @package io.shinhanlife.dap.mcg.tool.large
* @className PaginationRequestValidator
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;

View File

@@ -1,5 +1,20 @@
package io.shinhanlife.axhub.biz.mcp.gateway.tool.result; package io.shinhanlife.dap.mcg.tool.result;
/**
* @package io.shinhanlife.dap.mcg.tool.result
* @className ToolExecutionResult
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import java.util.List; import java.util.List;

Some files were not shown because too many files have changed in this diff Show More