Compare commits
263 Commits
993ced0813
...
feature/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 05ea5f9aaa | |||
| 50315ad245 | |||
|
|
65222b5b1a | ||
|
|
b9f98e89ce | ||
|
|
96e308aefb | ||
|
|
96a26bc93d | ||
|
|
a2586fd6d3 | ||
|
|
cdfb6ef901 | ||
|
|
3729e2d6ee | ||
|
|
946ec6d70f | ||
|
|
136ad2fa71 | ||
|
|
3e69436bfe | ||
|
|
f827760513 | ||
|
|
1a70495686 | ||
|
|
39c1f4ee12 | ||
|
|
e858859021 | ||
|
|
a58af1606e | ||
|
|
08ccc043da | ||
|
|
4393071212 | ||
|
|
1bac8d9d87 | ||
|
|
31e6fd605d | ||
|
|
15262d5a0f | ||
|
|
2957272cc3 | ||
|
|
52f585fa45 | ||
|
|
bc86f36825 | ||
|
|
55dcffa2c9 | ||
|
|
be0314afe8 | ||
|
|
39eab0429e | ||
|
|
cf958622bd | ||
|
|
d858f2ab90 | ||
|
|
2bc896a130 | ||
| b22cd779cf | |||
|
|
b3908f771f | ||
|
|
58234b1c59 | ||
|
|
541d6d1938 | ||
| 628925dce9 | |||
|
|
9d16b8ef3a | ||
|
|
39f68ccf46 | ||
|
|
1bf09705d1 | ||
|
|
60055dd9ed | ||
|
|
b5ff930556 | ||
|
|
2c4c447a8f | ||
|
|
68e1bf95fd | ||
|
|
204dd04d5f | ||
|
|
5fe8590695 | ||
| 0a2464d426 | |||
|
|
a667431de1 | ||
| aca0e009bc | |||
| df537fa77c | |||
|
|
8b4d4da0b9 | ||
|
|
c551b15378 | ||
|
|
b448e68f45 | ||
|
|
b68fbb9726 | ||
| c6a2f82455 | |||
|
|
2f5dad05df | ||
| ea1aea81bb | |||
|
|
b0bf4a9c68 | ||
|
|
752aae4dde | ||
|
|
5cb37cfda6 | ||
|
|
2a4b3e68e9 | ||
|
|
ca1082f1c2 | ||
|
|
7b1d3131e6 | ||
|
|
139ddbc6ca | ||
|
|
870ebb723f | ||
|
|
a08a7d25df | ||
|
|
60acc5df81 | ||
|
|
f5be5b2514 | ||
|
|
aae4898d18 | ||
|
|
06b724459e | ||
|
|
4133b63595 | ||
|
|
1e18d2bc47 | ||
|
|
0c89a09ccf | ||
|
|
3ce9201cd5 | ||
|
|
be81c095c9 | ||
|
|
d4554ce34f | ||
|
|
b6d4ab1bec | ||
|
|
2c8497ebee | ||
|
|
8a93d5d6dc | ||
|
|
c0de1f4edb | ||
|
|
1ee9f9e802 | ||
|
|
25f0d2bab5 | ||
|
|
9b9159a605 | ||
|
|
c8dd43221e | ||
|
|
7b8c38489e | ||
|
|
63187429b4 | ||
|
|
d4953d60ec | ||
|
|
72891dc2db | ||
|
|
82c27bd468 | ||
|
|
ff5ca4fc7a | ||
|
|
ba39a23b09 | ||
|
|
579979b5fe | ||
|
|
db46f30e72 | ||
|
|
2c64d86a56 | ||
|
|
ded5ef9e8f | ||
|
|
a6ca2b82a7 | ||
|
|
94cdaaa8c9 | ||
|
|
bea517c4c1 | ||
|
|
ad950071b2 | ||
|
|
e6407b07a2 | ||
|
|
e58c004d9d | ||
|
|
92fe2399be | ||
|
|
7ba18f7809 | ||
|
|
4dcc5a2978 | ||
|
|
994f5b4348 | ||
|
|
0a50b20960 | ||
|
|
97ba0c3267 | ||
|
|
388fa33c67 | ||
|
|
4015207b86 | ||
|
|
4dcc85eff0 | ||
|
|
72f04aef62 | ||
|
|
b5565a4da0 | ||
|
|
c38d59887c | ||
|
|
f3639043fc | ||
|
|
e278602d32 | ||
|
|
f6f86e5afb | ||
|
|
a89843518b | ||
|
|
d7b80a644c | ||
|
|
05a242e5de | ||
|
|
a409652bdf | ||
|
|
fa268edf7d | ||
|
|
c69f11e1c9 | ||
|
|
56483d804a | ||
|
|
1c353ce3a7 | ||
|
|
35f361a976 | ||
|
|
9066c621a0 | ||
|
|
4ed534b1d6 | ||
|
|
0892597c31 | ||
|
|
6416d3f844 | ||
|
|
dbe479a64b | ||
|
|
53f5047c8e | ||
|
|
f56e503a29 | ||
|
|
aae95591a0 | ||
|
|
227edf9744 | ||
|
|
12ac05c805 | ||
|
|
74ea89bf1c | ||
|
|
d562e35ad8 | ||
|
|
fc8c3da666 | ||
|
|
438c190447 | ||
|
|
51f37cfcbc | ||
|
|
44c2fcebba | ||
|
|
2758f6da8e | ||
|
|
8b11c1aae8 | ||
|
|
60a7226199 | ||
|
|
b3bdaa2b28 | ||
|
|
260f59b143 | ||
|
|
ad2ab985d8 | ||
|
|
18959b5128 | ||
|
|
7f85b10938 | ||
|
|
b5dbcc54f0 | ||
|
|
abf7a435a4 | ||
|
|
f0a295632f | ||
|
|
cec56e015b | ||
|
|
1e54370fb8 | ||
|
|
408a3f6b28 | ||
|
|
541150d3f4 | ||
|
|
165a6340ea | ||
|
|
5f8228bfce | ||
|
|
6893bb6b35 | ||
|
|
3c183e901b | ||
|
|
2948465b8a | ||
|
|
2b4adb45d2 | ||
|
|
323d227eee | ||
|
|
74b50482bf | ||
|
|
dfa5a73b20 | ||
|
|
86302d0947 | ||
|
|
ad37714c5b | ||
|
|
b57d96f83f | ||
|
|
fabbd368a2 | ||
|
|
f3cdfaccc3 | ||
|
|
e42ca92811 | ||
|
|
c18efe251e | ||
|
|
8d4e8fd976 | ||
|
|
edec314a83 | ||
|
|
da09997452 | ||
|
|
5c15ae3505 | ||
|
|
3eebf07b0e | ||
|
|
a161dcf319 | ||
|
|
ae3352ccb4 | ||
|
|
2deffd3f10 | ||
|
|
f334b8ecc5 | ||
|
|
1db19344bc | ||
|
|
8e6682d089 | ||
|
|
6968c7dfb7 | ||
|
|
416b068fcb | ||
|
|
933ba2ecfa | ||
|
|
ce96137948 | ||
|
|
e983b47af6 | ||
|
|
1796370a35 | ||
|
|
680dbe098b | ||
|
|
a877103d93 | ||
|
|
6e1ef40380 | ||
|
|
a59249e1db | ||
|
|
128fc3d022 | ||
|
|
899053aef5 | ||
|
|
0a5bc52e6a | ||
|
|
885963c81c | ||
|
|
b8a8da3b3c | ||
|
|
1a30c3a0cd | ||
|
|
50b6b8b9b0 | ||
|
|
140be4c7e9 | ||
|
|
75b93d47c2 | ||
|
|
f880f391d5 | ||
|
|
8a53bff74f | ||
|
|
a46943ca23 | ||
|
|
11b8b08890 | ||
|
|
2138ab7efa | ||
|
|
7dca243380 | ||
|
|
c90654bd1b | ||
|
|
334af3e40e | ||
|
|
ea27ed621a | ||
|
|
fbe7091975 | ||
|
|
7ccca1812f | ||
|
|
123fdb30a9 | ||
|
|
0ed927f845 | ||
|
|
60ac3bc4be | ||
|
|
0afa489165 | ||
|
|
8cd07a255c | ||
|
|
cc98d173ad | ||
|
|
c450f26d53 | ||
|
|
7660db3c38 | ||
|
|
beed6e7001 | ||
|
|
8612dd6ea7 | ||
|
|
db6ed5b09a | ||
|
|
db949be3f3 | ||
|
|
9edcdab450 | ||
|
|
82b1e6e267 | ||
|
|
e35b378d61 | ||
|
|
f8d6ed8da3 | ||
|
|
437d061bef | ||
|
|
9cce77a142 | ||
|
|
115aafa220 | ||
|
|
da7ed2e28b | ||
|
|
48ac7b9f9f | ||
|
|
580edd98a6 | ||
|
|
b9966bbbda | ||
|
|
438e30f987 | ||
|
|
fe9e00b5fb | ||
|
|
b0f12b578d | ||
|
|
39513a1470 | ||
|
|
821b249d50 | ||
|
|
e02a4e24e8 | ||
|
|
6b8ca5581f | ||
|
|
d6869f8191 | ||
|
|
515ab409ec | ||
|
|
b55f863de9 | ||
|
|
80ff392b5a | ||
|
|
6dd1fe6cda | ||
|
|
2162587a46 | ||
|
|
736593c516 | ||
|
|
bd85ca9f88 | ||
|
|
d6801b7ba0 | ||
|
|
2e6b28e1c1 | ||
|
|
0ebac4e375 | ||
|
|
8359371122 | ||
|
|
7eef3a65c5 | ||
|
|
467289bf9f | ||
|
|
c05bfb0ab3 | ||
|
|
ac15d38468 | ||
|
|
87f367b2c1 | ||
|
|
5a5e67629d | ||
|
|
e1591761d2 | ||
|
|
c982696631 | ||
|
|
2c0c0a3790 |
@@ -5,9 +5,10 @@
|
||||
|
||||
## 개발 가이드라인
|
||||
* 패키지명은 `controller` 대신 `presentation`을 사용합니다.
|
||||
* MapStruct 사용 시, 테스트 환경 에러를 방지하기 위해 생성자(`new ...Impl()`) 대신 `Mappers.getMapper(인터페이스명.class)` 방식으로 인스턴스를 가져옵니다.
|
||||
* MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트)
|
||||
|
||||
## 명심해야 할 규칙 추가란
|
||||
* `application.yml` 등 설정 파일 수정 시 한글이 깨지지 않도록 항상 UTF-8 인코딩을 유지하고, 깨진 문자열(`?\uFFFD` 등)이 발생하지 않도록 각별히 주의한다.
|
||||
* 이모지는 무조건 넣지 않는다
|
||||
* import 할것 무조건 한다
|
||||
* Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다.
|
||||
@@ -16,13 +17,13 @@
|
||||
* @package io.shinhanlife.axhub.biz.mcp.tool.sms
|
||||
* @className AxHubToolSmsApplication
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -30,3 +31,9 @@
|
||||
* 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외)
|
||||
* 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행)
|
||||
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다.
|
||||
|
||||
* 신한라이프 표준 로그 기준 (Logback 설정):
|
||||
* **로그 생성 경로:** `/swlog/어플리케이션명(모듈명)/코드명/` (예: `/swlog/dap-gateway/A01/`)
|
||||
* **로그 네이밍 규칙:** `${HOSTNAME}_코드명_yyyyMMdd.log` (예: `${HOSTNAME}_A01_20260722.log`)
|
||||
* **기본 로그 구분 코드:** 시스템 운영기록 가동기록의 경우 `A01`을 기본으로 사용한다.
|
||||
* **호스트명 동적 할당:** Logback 설정 시 `<property name="HOSTNAME" value="${HOSTNAME}" />` 를 선언하여 사용한다.
|
||||
|
||||
@@ -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>
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
16
.agents/skills/chaos-tool-tester/SKILL.md
Normal file
16
.agents/skills/chaos-tool-tester/SKILL.md
Normal 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 에러 메시지를 반환했는지 마크다운 표로 정리해서 보고할 것.
|
||||
12
.agents/skills/gateway-routing-tracer/SKILL.md
Normal file
12
.agents/skills/gateway-routing-tracer/SKILL.md
Normal 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. 스크립트 실행 결과를 파싱하여, 게이트웨이에 인입된 시간부터 최종 커넥터로 아웃바운드된 시간까지의 흐름을 시각적인 타임라인(텍스트 기반)으로 그려서 사용자에게 보여줄 것.
|
||||
18
.agents/skills/gateway-routing-tracer/trace.sh
Normal file
18
.agents/skills/gateway-routing-tracer/trace.sh
Normal 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}'
|
||||
16
.agents/skills/mcp-schema-autogen/SKILL.md
Normal file
16
.agents/skills/mcp-schema-autogen/SKILL.md
Normal 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 형태로만 출력하고, 불필요한 부연 설명은 생략한다.
|
||||
249
.agents/skills/mcp_best_practices/SKILL.md
Normal file
249
.agents/skills/mcp_best_practices/SKILL.md
Normal 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
|
||||
602
.agents/skills/mcp_evaluation/SKILL.md
Normal file
602
.agents/skills/mcp_evaluation/SKILL.md
Normal 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
|
||||
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
.gradle
|
||||
.idea
|
||||
/build/
|
||||
target/
|
||||
*/target/
|
||||
bin/
|
||||
*/bin/
|
||||
out/
|
||||
*/out/
|
||||
67
.gitea/workflows/deploy.yml
Normal file
67
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
name: Deploy to OCIWP
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Prepare Environment (Install Node.js & Git & Java 21)
|
||||
run: |
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y nodejs git openjdk-21-jdk rsync
|
||||
elif command -v apk &> /dev/null; then
|
||||
apk add --no-cache nodejs git openjdk21 rsync
|
||||
fi
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Sync Code to Host Volume
|
||||
run: |
|
||||
echo "Copying latest code to /app (Host Volume) and removing stale files..."
|
||||
if command -v rsync &> /dev/null; then
|
||||
rsync -a --delete --exclude='.git' . /app/
|
||||
else
|
||||
rm -rf /app/dap-* /app/src /app/build.gradle /app/settings.gradle
|
||||
cp -a . /app/
|
||||
fi
|
||||
|
||||
- name: Deploy Task on Host
|
||||
run: |
|
||||
echo "Starting Standard CI/CD Deploy pipeline..."
|
||||
|
||||
# 1. runner에 설치된 java로 직접 빌드! (격리된 환경 문제 해결)
|
||||
cd /app
|
||||
chmod +x gradlew
|
||||
./gradlew bootJar -x test
|
||||
|
||||
# 2. 빌드 결과 plain jar 아카이브 정리
|
||||
find /app -name "*-plain.jar" -delete
|
||||
|
||||
# 3. 좀비 컨테이너 청소 (현재 우리가 쓸 포트를 점유 중인 옛날 컨테이너들만 정확히 색출해서 사살)
|
||||
echo "Finding and killing zombie containers holding our ports..."
|
||||
for port in 6379 8080 8089 8281 8282 8283 8284 8285 8288; do
|
||||
cid=$(docker ps -q --filter "publish=$port")
|
||||
if [ ! -z "$cid" ]; then
|
||||
echo "Force killing container $cid using port $port"
|
||||
docker rm -f $cid
|
||||
fi
|
||||
done
|
||||
|
||||
# 4. 마운트된 /app 디렉토리로 이동하여 호스트의 도커 컴포즈 제어!
|
||||
cd /app
|
||||
docker system prune -f
|
||||
ACTIVE_PROFILE=dev docker compose up -d --build --remove-orphans gateway redis mci-mock dozzle was-sms was-oth
|
||||
|
||||
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 자동 소거 청소!
|
||||
docker image prune -f
|
||||
|
||||
echo "CI/CD Deploy Success!"
|
||||
62
.gitignore
vendored
62
.gitignore
vendored
@@ -56,11 +56,69 @@ build/
|
||||
# ===== VS Code =====
|
||||
.vscode/
|
||||
|
||||
# ===== 鍮뚮뱶 寃곌낵臾?=====
|
||||
target/
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
*.class
|
||||
*.jar
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
|
||||
# ===== 濡쒓렇 =====
|
||||
*.log
|
||||
logs/
|
||||
spring-shell.log
|
||||
|
||||
# ===== Maven =====
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
|
||||
# ===== IntelliJ IDEA =====
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# ===== Eclipse / STS =====
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
|
||||
# ===== NetBeans =====
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
.gradle/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
# ===== VS Code =====
|
||||
.vscode/
|
||||
|
||||
# ===== OS =====
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? =====
|
||||
# application-local.properties
|
||||
# application-secret.properties
|
||||
# application-local.yml
|
||||
# application-secret.yml
|
||||
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,30 +1,31 @@
|
||||
# 1. 鍮뚮뱶 ?섍꼍 (JDK 21)
|
||||
# 1. ??<3F><><EFBFBD>???<3F>꼍 (JDK 21)
|
||||
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Gradle ?섑띁? ?뚯뒪 蹂듭궗
|
||||
# Gradle ??<3F>띁?? ???<3F><> 蹂듭<EFBFBD>?
|
||||
COPY gradlew .
|
||||
COPY gradle gradle
|
||||
COPY build.gradle settings.gradle ./
|
||||
COPY src src
|
||||
|
||||
# 沅뚰븳 遺??諛?鍮뚮뱶 (?뚯뒪???쒖쇅)
|
||||
# 沅뚰<EFBFBD>??<3F>??<3F>???<3F><><EFBFBD>?(???<3F><>????<3F>쇅)
|
||||
RUN chmod +x gradlew
|
||||
RUN ./gradlew clean build -x test
|
||||
|
||||
# 2. ?ㅽ뻾 ?섍꼍 (JRE 21)
|
||||
# 2. ??<3F>뻾 ??<3F>꼍 (JRE 21)
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
|
||||
# ??꾩〈 ?ㅼ젙 (?쒓뎅 ?쒓컙)
|
||||
# ???꾩<EFBFBD>???<3F>젙 (??<3F>뎅 ??<3F>컙)
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ=Asia/Seoul
|
||||
|
||||
# 鍮뚮뱶??JAR ?뚯씪 蹂듭궗
|
||||
# ??<3F><><EFBFBD>??JAR ???<3F><> 蹂듭<EFBFBD>?
|
||||
COPY --from=builder /app/build/libs/*.jar app.jar
|
||||
|
||||
# 湲곕낯 ?ы듃 ?몄텧
|
||||
# 湲곕???????몄텧
|
||||
EXPOSE 8081
|
||||
|
||||
# 而⑦뀒?대꼫 ?ㅽ뻾 ??JAR ?ㅽ뻾
|
||||
# ?<3F>⑦???<3F><>???<3F>뻾 ??JAR ??<3F>뻾
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/**
|
||||
* @package io.shinhanlife
|
||||
* @className McpBridge
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
|
||||
692
README.md
692
README.md
@@ -1,164 +1,606 @@
|
||||
# DAP Backend
|
||||
# AX HUB MCP Tool
|
||||
|
||||
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다.
|
||||
신한라이프 업무 시스템과 AI Agent를 연결하는 MCP(Model Context Protocol) Gateway 및 Tool 서버 프로젝트입니다.
|
||||
|
||||
---
|
||||
Agent는 Gateway에서 Tool 목록과 입력 명세를 받고, Gateway는 권한과 정책을 확인한 뒤 Tool 서버로 요청을 전달합니다. 업무 Tool은 `DTO → UseCase → Converter → MCI/EAI Client` 구조로 레거시 시스템을 호출합니다.
|
||||
|
||||
## 아키텍처 개요 (Architecture Overview)
|
||||
## 전체 흐름
|
||||
|
||||
DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
|
||||
```text
|
||||
AI Agent / MCP Client
|
||||
│
|
||||
▼
|
||||
MCP Gateway (dap-gateway)
|
||||
├─ Tool 목록·스키마 제공
|
||||
├─ Tool 권한·승인·가드레일 확인
|
||||
├─ Redis Registry 및 실행 추적
|
||||
└─ 대상 Tool 서버로 라우팅
|
||||
│
|
||||
▼
|
||||
Tool Server (dap-tool-sms / dap-tool-oth)
|
||||
└─ BusinessToolController
|
||||
│
|
||||
▼
|
||||
UseCase → Converter → MCI/EAI Client → 레거시 시스템
|
||||
```
|
||||
|
||||
1. **MCP Gateway (`DapGatewayApplication`)**: 외부 LLM(Claude, GPT 등) 서버의 MCP 통신을 받아, 내부 Tool 서버들로 분배(라우팅)하는 허브 서버이자 관리자 웹(Scaffolder)을 제공하는 통합 서버 (포트: 8081)
|
||||
2. **MCP Tool (`DapTool*Application`)**: 실제 레거시 시스템(MCI, EAI 등)과 통신하여 비즈니스 로직(결제, 휴가신청 등)을 수행하는 어댑터 서버 (포트: 8082~8085 등 분산 구성 가능)
|
||||
## 현재 구조와 목표 구조
|
||||
|
||||
---
|
||||
현재는 Gateway와 두 개의 Tool 애플리케이션으로 구성됩니다.
|
||||
|
||||
## 환경 (Environment)
|
||||
```text
|
||||
현재: Gateway + SMS Tool Pod + OTH Tool Pod
|
||||
목표: Gateway + 고객 Pod + 영업 Pod + 지급/납입 Pod + 알림 Pod + 인사 Pod + 공통 Pod
|
||||
```
|
||||
|
||||
| 항목 | 버전 |
|
||||
|------|------|
|
||||
`dap-tool-oth`에는 여러 업무 카테고리가 함께 있습니다. AA 협의 후에는 부서·업무 소유권 단위로 Tool 서버, 이미지, Pod, 배포 파이프라인을 분리합니다. 이 목표 구조는 향후 전환 방향이며 현재 구현 완료 상태가 아닙니다.
|
||||
|
||||
## Gradle 멀티모듈
|
||||
|
||||
| 모듈 | 역할 | 실행 포트 |
|
||||
|---|---|---:|
|
||||
| `dap-gateway` | MCP 진입점, Tool Registry, 라우팅, 권한·가드레일, Chat API | 8081 |
|
||||
| `dap-tool-core` | 공통 어노테이션, Controller, JSON Schema, MCI/EAI 지원, 보안·로깅 공통 기능 | 라이브러리 |
|
||||
| `dap-tool-sms` | SMS/알림 Tool 서버 | 8082 |
|
||||
| `dap-tool-oth` | 공통·업무·샘플·MCI Tool 서버 | 8084 |
|
||||
|
||||
기술 기준은 Java 21, Spring Boot 4, Gradle, Spring AI MCP Server, Redis, MapStruct, MyBatis, Resilience4j입니다.
|
||||
|
||||
## 환경 (Environment)
|
||||
|
||||
| 항목 | 버전 / 기준 |
|
||||
|---|---|
|
||||
| Java | 21 |
|
||||
| Spring Boot | 4.0.5 |
|
||||
| Build Tool | Gradle |
|
||||
| 주요 기술 스택 | MyBatis, Lombok, MapStruct, P6Spy |
|
||||
| 데이터베이스 | H2 (in-memory, 로컬 개발용) |
|
||||
| 세션/캐시 저장소 | Redis |
|
||||
| **장애 격리 / 제어** | **Resilience4j (RateLimiter, CircuitBreaker, Retry)** |
|
||||
| **메시지 큐** | **Kafka (트래픽 폭주 시 대기열 전환용)** |
|
||||
| Gradle Wrapper | 8.14.3 |
|
||||
| Spring AI BOM | 2.0.0 |
|
||||
| Spring AI MCP Server | `spring-ai-starter-mcp-server-webmvc` |
|
||||
| Redis Client | Lettuce 6.6.0.RELEASE |
|
||||
| Resilience | Resilience4j 2.2.0 |
|
||||
| MyBatis Spring Boot Starter | 3.0.3 |
|
||||
| MapStruct | 1.5.5.Final |
|
||||
| Lombok | 1.18.32 |
|
||||
| JSON Schema Validator | networknt 1.4.0 |
|
||||
| OpenAPI UI | springdoc 2.5.0 |
|
||||
| 컨테이너 실행 | Docker Compose |
|
||||
|
||||
프로젝트는 JDK 21을 기준으로 컴파일됩니다. IntelliJ에서는 Project SDK, Gradle JVM, Run Configuration JRE를 모두 JDK 21로 맞춰야 합니다.
|
||||
## 5분 빠른 시작
|
||||
|
||||
Docker와 JDK 21이 준비된 로컬 개발 환경 기준입니다.
|
||||
|
||||
```powershell
|
||||
# 1. Redis와 MCI Mock만 먼저 실행
|
||||
$env:ACTIVE_PROFILE = 'local'
|
||||
docker compose up -d redis mci-mock
|
||||
|
||||
# 2. Gateway 실행 (새 PowerShell)
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
$env:OPENROUTER_API_KEY = '<개발용 비밀 저장소의 키>'
|
||||
.\gradlew.bat :dap-gateway:bootRun
|
||||
|
||||
# 3. OTH Tool 실행 (또 다른 PowerShell)
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
|
||||
.\gradlew.bat :dap-tool-oth:bootRun
|
||||
```
|
||||
|
||||
Tool 서버가 기동된 뒤 아래 URL로 등록된 Tool 목록을 확인합니다.
|
||||
|
||||
```text
|
||||
http://localhost:8081/mcp/api/v1/tools/list
|
||||
```
|
||||
|
||||
SMS Tool도 함께 확인하려면 별도 PowerShell에서 아래 명령을 실행합니다.
|
||||
|
||||
```powershell
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8082'
|
||||
.\gradlew.bat :dap-tool-sms:bootRun
|
||||
```
|
||||
|
||||
전체 컨테이너 환경이 필요하면 개별 실행 대신 다음 한 줄을 사용합니다.
|
||||
|
||||
```powershell
|
||||
$env:ACTIVE_PROFILE = 'local'
|
||||
docker compose up -d --build
|
||||
```
|
||||
## Tool 등록과 실행
|
||||
|
||||
### 등록과 목록 제공
|
||||
|
||||
1. Tool 서버 기동 시 `ToolRegistryHeartbeatSender`가 `@McpTool`, `@McpFunction`을 스캔합니다.
|
||||
2. Tool 이름, 설명, 입력 JSON Schema, `categoryKey`, 연동 방식, 실행 URL을 메타데이터로 생성합니다.
|
||||
3. Gateway Redis Registry에 등록·Heartbeat 정보를 전송합니다.
|
||||
4. Agent와 관리 화면은 Gateway에서 Tool 목록과 명세를 조회합니다.
|
||||
|
||||
### 실행
|
||||
|
||||
1. Agent가 Gateway에 Tool 이름과 입력값을 보냅니다.
|
||||
2. Gateway가 Tool 존재 여부, 허용 Tool, 쓰기 승인, 가드레일을 확인합니다.
|
||||
3. Gateway가 Tool 서버의 `/mcp/{toolName}`으로 요청을 전달합니다.
|
||||
4. `BusinessToolController`가 Tool 메서드를 찾아 DTO로 변환하고 JSON Schema를 검증합니다.
|
||||
5. UseCase가 업무 흐름을 수행합니다.
|
||||
6. Converter가 업무 DTO를 인터페이스 ID 기반 MCI 요청 DTO로 변환합니다.
|
||||
7. MCI/EAI Client가 레거시를 호출하고 결과를 Tool 응답으로 반환합니다.
|
||||
|
||||
## 주요 URL
|
||||
|
||||
로컬에서 Gateway를 직접 실행할 때의 기준입니다. Docker Compose를 사용하면 Gateway 호스트 포트는 `8281`입니다.
|
||||
|
||||
| 용도 | 메서드 | URL |
|
||||
|---|---|---|
|
||||
| Tool 목록 | `GET` | `http://localhost:8081/mcp/api/v1/tools/list` |
|
||||
| Tool 실행 | `POST` | `http://localhost:8081/mcp/api/v1/tools/call` |
|
||||
| Tool Markdown 문서 | `GET` | `http://localhost:8081/mcp/api/v1/tools/docs/markdown` |
|
||||
| 카테고리별 SSE MCP 연결 | `GET` | `http://localhost:8081/mcp/sse/{categoryKey}` |
|
||||
| 카테고리별 MCP 호출 채널 | `POST` | `http://localhost:8081/mcp/custom/{categoryKey}` |
|
||||
| Chat 스트리밍 API | `POST` | `http://localhost:8081/api/chat` |
|
||||
| Scaffold API | `POST` | `http://localhost:8081/api/v1/scaffold/pod` 또는 `/tool` |
|
||||
|
||||
`/mcp/sse/{categoryKey}`는 SSE 연결을 여는 전송 경로이고, `/mcp/custom/{categoryKey}`는 같은 카테고리의 MCP 요청을 처리하는 호출 경로입니다. 두 URL은 역할이 다릅니다.
|
||||
|
||||
## Tool 개발 표준
|
||||
|
||||
| 구성 요소 | 책임 |
|
||||
|---|---|
|
||||
| `XxxRequest`, `XxxResponse` | Agent/Tool 관점의 입력·응답 DTO |
|
||||
| `XxxUseCase` | Tool 계약과 MCP 메타데이터 선언 |
|
||||
| `XxxUseCaseImpl` | 업무 흐름 조합과 Client 호출 |
|
||||
| `XxxConverter` | 업무 DTO와 레거시 인터페이스 DTO 사이 변환 |
|
||||
| `MciXxxClient` | Glow/MCI 또는 EAI 통신 호출 |
|
||||
| `INTERFACE_ID_I`, `INTERFACE_ID_O` | 인터페이스 ID 기준 MCI 요청·응답 DTO |
|
||||
|
||||
### Tool 선언
|
||||
|
||||
Tool 그룹에는 `@McpTool`, Agent가 호출하는 함수에는 `@McpFunction`을 사용합니다.
|
||||
|
||||
```java
|
||||
@McpTool(routingType = "MCI", categoryKey = "claim")
|
||||
public interface ClaimInquiryUseCase {
|
||||
|
||||
@McpFunction(
|
||||
name = "claim_inquiry",
|
||||
displayName = "보험금 청구 조회",
|
||||
description = "청구 번호로 보험금 청구 상태를 조회합니다.",
|
||||
mappingId = "CLM00000001"
|
||||
)
|
||||
ClaimInquiryResponse inquire(ClaimInquiryRequest request);
|
||||
}
|
||||
```
|
||||
|
||||
`categoryKey`는 Tool의 업무 그룹입니다. Gateway의 목록 필터링, Agent 권한, 동적 MCP 서버 구분에 사용하므로 합의된 업무 키를 사용합니다.
|
||||
|
||||
- `MCI`: 사내 MCI 인터페이스 호출
|
||||
- `EAI`: EAI 연동
|
||||
- `DIRECT`: 외부 HTTP 또는 내부 직접 연동
|
||||
|
||||
### 변환 원칙
|
||||
|
||||
UseCase 구현체는 Tool 요청을 레거시 요청과 섞어 쓰지 않습니다. Converter에서 변환한 뒤 Client에 전달합니다.
|
||||
|
||||
```java
|
||||
@Override
|
||||
public ClaimInquiryResponse inquire(ClaimInquiryRequest request) {
|
||||
CLM00000001_I mciRequest = converter.toMciRequest(request);
|
||||
CLM00000001_O mciResponse = mciClmClient.callClm00000001(mciRequest);
|
||||
return converter.toResponse(mciResponse);
|
||||
}
|
||||
```
|
||||
|
||||
MCI 입출력 객체는 업무 이름이 아니라 인터페이스 ID를 기준으로 둡니다.
|
||||
|
||||
```text
|
||||
CLCNNB00001_I : CLCNNB00001 요청 DTO
|
||||
CLCNNB00001_O : CLCNNB00001 응답 DTO
|
||||
```
|
||||
|
||||
예를 들어 `Onnba3011Request`와 `CLCNNB00001_I`는 같은 업무 데이터를 담을 수 있지만 같은 객체가 아닙니다. 둘 사이의 변환 책임은 `Onnba3011Converter`에 둡니다.
|
||||
|
||||
### 새 Tool 추가 체크리스트
|
||||
|
||||
새 업무 Tool을 추가할 때는 아래 순서로 확인합니다.
|
||||
|
||||
- [ ] 소속 모듈과 `categoryKey`를 업무 소유 조직 기준으로 결정한다.
|
||||
- [ ] Agent 입력·응답 DTO인 `XxxRequest`, `XxxResponse`를 만든다.
|
||||
- [ ] `XxxUseCase`에 `@McpTool`을 선언하고, 호출 메서드에 `@McpFunction`의 이름·설명·연동 ID를 선언한다.
|
||||
- [ ] `XxxUseCaseImpl`에서 업무 흐름만 조합한다.
|
||||
- [ ] `XxxConverter`에 업무 DTO ↔ 인터페이스 ID DTO 변환을 둔다.
|
||||
- [ ] `MciXxxClient`와 `INTERFACE_ID_I`, `INTERFACE_ID_O`를 인터페이스 ID 기준으로 만든다.
|
||||
- [ ] DTO에 Bean Validation을 선언하고, 중첩 DTO가 있으면 입력 Schema와 검증 대상에 포함되는지 확인한다.
|
||||
- [ ] 조회·변경 작업 특성에 따라 `readOnlyHint`, `requiresApproval`, `idempotentHint`를 설정한다.
|
||||
- [ ] 단위 테스트를 작성하고 `:dap-tool-core:test` 또는 대상 모듈 테스트를 실행한다.
|
||||
- [ ] Tool 서버 기동 후 `/mcp/api/v1/tools/list`에서 이름, 설명, category, JSON Schema가 맞는지 확인한다.
|
||||
- [ ] 요청·응답 로그에 개인정보나 인증값이 남지 않는지 확인한다.
|
||||
### 등록·노출 제어
|
||||
|
||||
| 속성 | 의미 |
|
||||
|---|---|
|
||||
| `register` | Redis Registry와 Gateway 카탈로그에 등록할지 여부 |
|
||||
| `visible` | Agent/클라이언트 목록에 표시할지 여부 |
|
||||
| `requiresApproval` | 쓰기·고위험 작업의 승인 요구 여부 |
|
||||
| `readOnlyHint`, `destructiveHint`, `idempotentHint` | Agent 호출 특성 힌트 |
|
||||
|
||||
`register = false`인 Tool은 자동 카탈로그 등록 대상이 아닙니다. 별도 실행 목적이 있는 경우에만 사용하고, 필요한 fallback 경로를 운영 설정으로 확인합니다.
|
||||
|
||||
## 빌드·테스트·로컬 실행
|
||||
|
||||
### 사전 조건
|
||||
|
||||
- JDK 21
|
||||
- Gradle Wrapper 사용 권장
|
||||
- 로컬 Redis 또는 Docker Compose 환경
|
||||
- 필요 시 MCI Mock 또는 사내 MCI/EAI 접근 환경
|
||||
|
||||
### 전체 빌드와 대표 검증
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat clean build
|
||||
.\gradlew.bat :dap-tool-core:test
|
||||
.\gradlew.bat :dap-tool-core:compileJava
|
||||
```
|
||||
|
||||
### 애플리케이션 실행
|
||||
|
||||
각 애플리케이션은 별도 PowerShell에서 실행합니다.
|
||||
|
||||
```powershell
|
||||
# Gateway
|
||||
.\gradlew.bat :dap-gateway:bootRun
|
||||
|
||||
# SMS Tool
|
||||
.\gradlew.bat :dap-tool-sms:bootRun
|
||||
|
||||
# 기타 업무 Tool
|
||||
.\gradlew.bat :dap-tool-oth:bootRun
|
||||
```
|
||||
|
||||
기본 프로필은 `local`입니다. 개발 서버 설정이 필요하면 실행 환경에 프로필을 지정합니다.
|
||||
|
||||
```powershell
|
||||
$env:SPRING_PROFILES_ACTIVE = 'dev'
|
||||
.\gradlew.bat :dap-tool-oth:bootRun
|
||||
```
|
||||
|
||||
## Docker Compose 실행
|
||||
|
||||
Docker Compose는 Redis, Gateway, MCI Mock, SMS Tool, OTH Tool을 함께 기동합니다.
|
||||
|
||||
```powershell
|
||||
$env:ACTIVE_PROFILE = 'local'
|
||||
docker compose up -d --build
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
| 서비스 | 컨테이너 포트 | 호스트 포트 |
|
||||
|---|---:|---:|
|
||||
| Redis | 6379 | 6379 |
|
||||
| Gateway | 8081 | 8281 |
|
||||
| MCI Mock | 8080 | 8089 |
|
||||
| SMS Tool | 8082 | 8282 |
|
||||
| OTH Tool | 8084 | 8284 |
|
||||
|
||||
컨테이너 내부와 PC 브라우저의 접속 주소는 다릅니다.
|
||||
|
||||
```text
|
||||
컨테이너 내부: http://gateway:8081, http://tool-oth:8084
|
||||
PC 브라우저: http://localhost:8281, http://localhost:8284
|
||||
```
|
||||
|
||||
## 환경 설정
|
||||
|
||||
### 프로필
|
||||
|
||||
기본 프로필은 `local`입니다. Tool 서버와 Gateway 모두 `local`, `dev` 프로필 파일을 사용합니다.
|
||||
|
||||
| 프로필 | 목적 | 주요 설정 파일 |
|
||||
|---|---|---|
|
||||
| `local` | PC 개발·MCI Mock·H2 메모리 DB 기반 실행 | `application-local.yml` |
|
||||
| `dev` | 개발 서버 배포 실행 | `application-dev.yml` |
|
||||
|
||||
프로필은 환경 변수로 지정합니다.
|
||||
|
||||
```powershell
|
||||
# 로컬 개발
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
|
||||
# 개발 서버 설정으로 실행
|
||||
$env:SPRING_PROFILES_ACTIVE = 'dev'
|
||||
```
|
||||
|
||||
### 환경별 연결 주소 비교
|
||||
|
||||
| 항목 | `local` 프로세스 실행 | Docker Compose | `dev` 프로필 |
|
||||
|---|---|---|---|
|
||||
| Gateway 접근 주소 | `http://localhost:8081` | 컨테이너 내부 `http://gateway:8081` | 배포 환경 Gateway URL |
|
||||
| SMS Tool 주소 | `http://localhost:8082` | `http://tool-sms:8082` | `PORT` 기본 8082 |
|
||||
| OTH Tool 주소 | `http://localhost:8084` | `http://tool-oth:8084` | `PORT` 기본 8084 |
|
||||
| Redis 주소 | 로컬 Redis 또는 `localhost:6379` | `redis:6379` | 운영/개발 Redis 설정 |
|
||||
| MCI/EAI 대상 | MCI Mock 또는 로컬 설정 | `mci-mock:8080` | 개발망 연동 설정 |
|
||||
| 브라우저 Gateway 접속 | `http://localhost:8081` | `http://localhost:8281` | 운영·개발 도메인 |
|
||||
|
||||
`local`에서 프로세스를 직접 실행하면 Tool의 `AXHUB_GATEWAY_URL`은 `localhost`를 사용합니다. Docker에서는 각 컨테이너가 서로 다른 네트워크 공간에 있으므로 반드시 Compose 서비스 이름을 사용합니다.
|
||||
### Gateway 환경 변수
|
||||
|
||||
| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 |
|
||||
|---|---|---|---|
|
||||
| `SPRING_PROFILES_ACTIVE` | Gateway, 모든 Tool | 활성 Spring 프로필 | `local` |
|
||||
| `OPENROUTER_API_KEY` | Gateway | Chat/LLM 호출 API 키 | 운영·개발 환경의 비밀 저장소에서 주입 |
|
||||
| `SPRING_DATA_REDIS_HOST` | Gateway, 모든 Tool | Redis 호스트 | Docker: `redis` |
|
||||
| `SPRING_DATA_REDIS_PORT` | Gateway, 모든 Tool | Redis 포트 | `6379` |
|
||||
| `MCP_GATEWAY_FALLBACK_DEFAULT_URL` | Gateway | Registry에 없는 Tool의 기본 fallback URL | Docker: `http://tool-oth:8084` |
|
||||
| `MCP_GATEWAY_FALLBACK_ROUTES_SMS` | Gateway | SMS 계열 fallback URL | Docker: `http://tool-sms:8082` |
|
||||
| `ACTIVE_PROFILE` | Docker Compose | Compose가 `SPRING_PROFILES_ACTIVE`에 전달할 프로필 | `local` |
|
||||
|
||||
`OPENROUTER_API_KEY` 같은 인증값은 `application.yml`, README, Git 커밋에 직접 넣지 않습니다. 개발·운영 환경의 Secret, CI/CD 변수 또는 안전한 환경 변수로 주입합니다.
|
||||
|
||||
### Tool 서버 환경 변수
|
||||
|
||||
| 변수 | 적용 대상 | 설명 | 로컬 기본값/예시 |
|
||||
|---|---|---|---|
|
||||
| `PORT` | `dap-tool-sms`, `dap-tool-oth` | 개발 프로필에서 Tool 서버 포트 변경 | SMS `8082`, OTH `8084` |
|
||||
| `AXHUB_GATEWAY_URL` | 모든 Tool | Tool 등록·Heartbeat 대상 Gateway 주소 | 로컬 `http://localhost:8081`, Docker `http://gateway:8081` |
|
||||
| `AXHUB_TOOL_URL` | 모든 Tool | Gateway가 해당 Tool Pod를 호출할 주소 | 로컬 `http://localhost:{server.port}` |
|
||||
| `GLOW_COMMUNICATION_MCI_HOMT` | Docker Tool 컨테이너 | MCI 대상 호스트 | 로컬 Compose는 `mci-mock` |
|
||||
| `GLOW_COMMUNICATION_MCI_PORT` | Docker Tool 컨테이너 | MCI 대상 포트 | 로컬 Compose는 `8080` |
|
||||
| `GLOW_COMMUNICATION_EAI_HOMT` | Docker Tool 컨테이너 | EAI 대상 호스트 | 로컬 Compose는 `mci-mock` |
|
||||
| `GLOW_COMMUNICATION_EAI_PORT` | Docker Tool 컨테이너 | EAI 대상 포트 | 로컬 Compose는 `8080` |
|
||||
|
||||
`AXHUB_TOOL_URL`은 반드시 Gateway가 실제로 접근 가능한 주소여야 합니다. PC에서 각각 실행할 때는 `localhost`를 사용하고, Docker 컨테이너 안에서는 `tool-sms`, `tool-oth` 같은 Compose 서비스 이름을 사용합니다.
|
||||
|
||||
### 로컬 실행용 권장 설정
|
||||
|
||||
아래는 키 값 없이 로컬 프로세스를 실행하는 예시입니다. Redis를 Docker로 먼저 기동하거나 전체 Docker Compose를 사용합니다.
|
||||
|
||||
```powershell
|
||||
# 선택 1: Redis만 기동
|
||||
$env:ACTIVE_PROFILE = 'local'
|
||||
docker compose up -d redis mci-mock
|
||||
|
||||
# 선택 2: 각 프로세스를 로컬에서 기동
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
$env:OPENROUTER_API_KEY = '<개인 또는 개발용 비밀 저장소의 키>'
|
||||
.\gradlew.bat :dap-gateway:bootRun
|
||||
```
|
||||
|
||||
다른 PowerShell에서 Tool을 실행합니다.
|
||||
|
||||
```powershell
|
||||
$env:SPRING_PROFILES_ACTIVE = 'local'
|
||||
$env:AXHUB_GATEWAY_URL = 'http://localhost:8081'
|
||||
$env:AXHUB_TOOL_URL = 'http://localhost:8084'
|
||||
.\gradlew.bat :dap-tool-oth:bootRun
|
||||
```
|
||||
|
||||
### 권한 도메인 설정
|
||||
|
||||
`mcp.security.tenant-domains`는 테넌트 또는 호출 주체가 접근할 수 있는 `categoryKey`를 정의합니다. 운영 환경에서는 `ALL`을 무분별하게 사용하지 않고, Agent·조직별 허용 도메인을 최소 권한으로 설정합니다.
|
||||
|
||||
```yaml
|
||||
mcp:
|
||||
security:
|
||||
tenant-domains:
|
||||
claims-agent: claim, customer
|
||||
notification-agent: notification
|
||||
```
|
||||
|
||||
## 공통 기능과 운영 기준
|
||||
|
||||
### Redis
|
||||
|
||||
Redis는 Tool Registry의 등록 상태와 Heartbeat, Gateway 실행 추적 정보에 사용됩니다. 현재 Redis는 PII 원문 보관소가 아닙니다.
|
||||
|
||||
### 권한과 승인
|
||||
|
||||
Gateway는 Agent가 전달한 허용 Tool 목록, 서버 정책, 신뢰된 Claim 여부, 쓰기 승인 여부를 확인합니다. Tool마다 권한 로직을 중복 구현하지 말고 Gateway 공통 정책과 `@McpFunction` 메타데이터를 사용합니다.
|
||||
|
||||
### Trace ID와 Request ID
|
||||
|
||||
```text
|
||||
trace-id : 사용자 요청 전체에서 유지되는 상관관계 ID
|
||||
request-id : Gateway → Tool, Tool → MCI 등 HTTP 호출마다 새로 생성되는 ID
|
||||
```
|
||||
|
||||
Gateway와 Tool에는 관련 헤더 및 MDC 기반 로그 처리가 있습니다. 신규 HTTP Client도 공통 전파 정책을 따르며, Tool별로 임의의 헤더 이름을 추가하지 않습니다.
|
||||
|
||||
### 로그와 개인정보
|
||||
|
||||
Gateway에는 민감 키와 일부 형식을 마스킹하는 공통 기능이 있습니다. 마스킹은 원문을 Agent에서 분리하는 PII 토큰화와는 다릅니다.
|
||||
|
||||
- 요청·응답 전문을 로그에 남길 때는 반드시 마스킹합니다.
|
||||
- 운영 로그에 주민번호, 계좌번호, 전화번호, 이메일, 인증값을 남기지 않습니다.
|
||||
- Tool 서버의 신규 로그도 같은 마스킹 정책을 적용합니다.
|
||||
- 민감정보 복원 필요 여부는 Tool 개발자가 임의로 결정하지 않고 보안·AA 정책을 따릅니다.
|
||||
|
||||
## AA 협의 기반 향후 전환 과제
|
||||
|
||||
아래 항목은 현재 구현 완료 기능이 아니라 회의에서 합의한 목표 구조입니다.
|
||||
|
||||
### 부서별 Tool Pod와 저장소 경계
|
||||
|
||||
현재 `dap-tool-oth`에 함께 있는 업무 Tool을 부서·업무 소유권 단위로 분리합니다. 각 Pod는 독립 이미지, 독립 배포, 독립 장애 범위를 갖도록 구성합니다. 실제 분리는 AA가 확정한 Tool 소유 부서와 운영 책임 매핑을 기준으로 수행합니다.
|
||||
|
||||
### Agent별 Tool 노출 수 제한
|
||||
|
||||
Agent에게 모든 Tool을 한 번에 제공하지 않고, 업무 도메인과 권한에 따라 약 10~20개 수준의 Tool 그룹을 제공합니다. `categoryKey`는 이를 위한 기초 메타데이터이며, 향후 Agent-Tool Group 정책으로 확장합니다.
|
||||
|
||||
### PII 토큰화
|
||||
|
||||
```text
|
||||
원문 개인정보
|
||||
→ PII Gateway가 Redis에 짧은 TTL로 보관
|
||||
→ Agent에는 PII 토큰 또는 안전한 식별자만 전달
|
||||
→ 인가된 Tool이 MCI 호출 직전에 필요한 항목만 복원
|
||||
```
|
||||
|
||||
이 전환 전까지는 현행 마스킹 기능을 PII 분리 구현으로 오해하지 않아야 합니다.
|
||||
|
||||
### MCP SDK와 Glow Framework
|
||||
|
||||
Gateway에는 Spring AI MCP Server 의존성이 포함되어 있습니다. MCP SDK/Glow 표준 적용 시에는 업무 Tool을 재작성하지 않고, 기존 `@McpTool`·`@McpFunction`과 UseCase를 표준 MCP Tool 명세·호출 콜백으로 연결하는 Adapter 계층을 공통 Core에 추가합니다.
|
||||
|
||||
```text
|
||||
MCP SDK 표준 tools/list, tools/call
|
||||
↓
|
||||
공통 Adapter
|
||||
↓
|
||||
기존 UseCase → Converter → MCI Client
|
||||
```
|
||||
|
||||
따라서 업무 DTO, Converter, MCI Client의 책임은 유지됩니다.
|
||||
|
||||
## 참고 소스
|
||||
|
||||
| 주제 | 대표 위치 |
|
||||
|---|---|
|
||||
| Gateway Tool API | `dap-gateway/.../presentation/McpRouterController.java` |
|
||||
| 동적 MCP SSE/호출 경로 | `dap-gateway/.../sync/DynamicMcpController.java` |
|
||||
| Tool 실행 Controller | `dap-tool-core/.../presentation/BusinessToolController.java` |
|
||||
| Tool 자동 등록 | `dap-tool-core/.../usecase/ToolRegistryHeartbeatSender.java` |
|
||||
| Tool 어노테이션 | `dap-tool-core/.../annotation/McpTool.java`, `McpFunction.java` |
|
||||
| Tool 예시 | `dap-tool-oth/.../biz/oth`, `biz/sol`, `biz/smp` |
|
||||
| SMS Tool 예시 | `dap-tool-sms/.../biz/sms` |
|
||||
| Docker 환경 | `docker-compose.yml` |
|
||||
|
||||
---
|
||||
|
||||
## ▶ 실행 방법 (How to Run)
|
||||
문서에 없는 업무·보안·배포 기준은 임의로 추가하지 말고 AA 및 플랫폼 운영 기준과 먼저 합의합니다.
|
||||
## Input/Output Schema 작성 가이드
|
||||
|
||||
### 1. Gateway & Tool 서버 실행 (MCP 연동용)
|
||||
- **Gateway 서버 기동:**
|
||||
- `./gradlew :dap-gateway:bootRun`
|
||||
- **Tool 서버 기동:**
|
||||
- `./gradlew :dap-tool-other:bootRun` (또는 dap-tool-payment 등)
|
||||
- Tool 서버가 기동되면 자동으로 Gateway(8081)에 자신을 등록(Auto-Registration)합니다.
|
||||
- **(선택) 특정 Tool 그룹만 실행하기:**
|
||||
- 업무 특성에 따라 세분화된 그룹에 속한 Tool만 띄우고 싶다면, 실행 인수에 `--mcp.tool.target=그룹명`을 추가합니다.
|
||||
- **지원되는 그룹명:**
|
||||
- `NOTIFICATION`: 이메일, SMS 발송
|
||||
- `CLAIM`: 청구 처리, 심사 상태 조회
|
||||
- `POLICY`: 증권 발행, 발행 가능 여부 조회
|
||||
- `HR`: 휴가 등록, 연차 갯수 조회
|
||||
- `CONTRACT`: 계약 상태, 계약 상세 조회
|
||||
- `CUSTOMER`: 고객 등급, 고객 상세 정보 조회
|
||||
- IntelliJ IDEA: `Run/Debug Configurations`에서 `DapTool*Application` 의 `Program arguments` 에 `--mcp.tool.target=NOTIFICATION` 입력
|
||||
Tool Schema는 Agent가 Tool을 정확히 호출하고, 반환값의 의미를 일관되게 해석하도록 하는 계약입니다. 인증 정보·사번·주민번호 등 민감정보(PII)는 Input/Output Schema와 Tool 응답에 포함하지 않습니다.
|
||||
|
||||
### Input Schema
|
||||
|
||||
---
|
||||
Input Schema는 Agent가 Tool에 전달하는 파라미터의 이름, 타입, 필수 여부, 허용값, 형식 등을 정의합니다.
|
||||
|
||||
## 🤖 AI Agent 연동 아키텍처 (MCP & Agent Builder)
|
||||
적용 우선순위는 다음과 같습니다.
|
||||
|
||||
본 시스템은 **투트랙(Two-Track) AI 연동 아키텍처**를 제공하여 로컬 개발 환경과 프로덕션 환경 모두를 완벽하게 지원합니다.
|
||||
1. `inputSchemaResource` — 복잡한 규칙을 담은 JSON Schema 리소스
|
||||
2. `inputSchema` — 어노테이션에 직접 선언한 JSON Schema
|
||||
3. 요청 DTO 필드의 `@McpValidation` — 자동 JSON Schema 생성
|
||||
|
||||
### 1. 로컬 코딩 AI (Antigravity, Cursor, Claude Desktop 등) 연동
|
||||
표준 MCP 통신(Stdio)을 요구하는 로컬 AI 에이전트를 위해 자바 기반의 브릿지 스크립트(`McpBridge.java`)를 내장하고 있습니다. 브릿지가 Stdio 요청을 HTTP로 변환하여 로컬 환경의 Gateway(포트: 8281)로 전달합니다.
|
||||
단순한 요청 DTO는 `@McpValidation`만으로 관리합니다.
|
||||
|
||||
- **설정 방법**: IDE의 `mcp_config.json` 설정 파일에 아래와 같이 등록합니다.
|
||||
```json
|
||||
"mcpServers": {
|
||||
"dap-gateway": {
|
||||
"command": "java",
|
||||
"args": ["C:/절대경로/dap-backend-main/McpBridge.java"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- **특정 카테고리 툴 필터링**: `McpBridge.java` 내부의 URI 파라미터(`?categoryKey=common`)를 수정하여 원하는 도메인의 툴만 선택적으로 AI에게 학습시킬 수 있습니다.
|
||||
```java
|
||||
public class ClaimSearchRequest {
|
||||
|
||||
### 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) 문서만 클라우드 콘솔에 등록하면 즉시 라이브 챗봇/에이전트로 서비스할 수 있습니다.
|
||||
@McpValidation(required = true, pattern = "^CLM[0-9]{13}$")
|
||||
private String claimNo;
|
||||
|
||||
---
|
||||
@McpValidation(minimum = 1, maximum = 100)
|
||||
private Integer size;
|
||||
}
|
||||
```
|
||||
|
||||
## 비공개 Tool 관리 및 Fallback 연동 (Visibility & Routing)
|
||||
### Output Schema
|
||||
|
||||
저희 시스템은 MSA 보안 및 아키텍처 원칙에 따라 Tool의 **레지스트리 등록 여부(라우팅)**와 **API 노출 여부(가시성)**를 완벽히 분리하여 관리합니다.
|
||||
Output Schema는 Tool이 반환하는 결과의 타입과 의미를 정의합니다. `BusinessToolController`는 Tool 실행 후 반환값을 Output Schema 기준으로 검증합니다.
|
||||
|
||||
1. **`visible = false`**:
|
||||
레지스트리에 정상적으로 등록되어 게이트웨이가 동적으로 라우팅하지만, 클라이언트에게 제공되는 `/tools/list` API 목록에서는 숨겨집니다.
|
||||
2. **`register = false`**:
|
||||
내부 레지스트리(Redis)에 툴 정보를 등록하지 않습니다 (외부 레지스트리를 독자적으로 사용할 경우 등).
|
||||
이 경우 게이트웨이는 `application.properties`의 `mcp.gateway.fallback.routes` 설정을 참조하여 **Fallback 정적 라우팅**을 수행하므로 연동이 100% 보장됩니다.
|
||||
적용 우선순위는 다음과 같습니다.
|
||||
|
||||
1. `outputSchemaResource` — 조건부 필드·중첩 배열 등 복잡한 규칙을 담은 JSON Schema 리소스
|
||||
2. `outputSchema` — 어노테이션에 직접 선언한 JSON Schema
|
||||
3. 반환 DTO의 `@McpOutputSchema`와 필드 `@McpValidation` — 자동 JSON Schema 생성
|
||||
4. 위 설정이 모두 없으면 Output Schema 검증을 수행하지 않음
|
||||
|
||||
따라서 단순한 응답은 별도 `outputSchemaResource` 없이 반환 DTO에 `@McpOutputSchema`를 선언하면 됩니다. `null`이 정상 값일 수 있는 필드는 `nullable = true`를 반드시 지정합니다.
|
||||
|
||||
```java
|
||||
@McpOutputSchema
|
||||
public class ClaimSearchResponse {
|
||||
|
||||
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
|
||||
private String resultCode;
|
||||
|
||||
@McpValidation(nullable = true, minimum = 0)
|
||||
private Long approvedAmount;
|
||||
}
|
||||
```
|
||||
|
||||
### 복잡한 Schema는 Tool 모듈별 리소스로 관리
|
||||
|
||||
조건부 응답, 중첩 DTO, 배열 정렬 기준처럼 어노테이션만으로 표현하기 어려운 규칙은 Tool Core가 아니라 각 Tool 모듈의 리소스에 JSON Schema로 둡니다.
|
||||
|
||||
```text
|
||||
src/main/resources/
|
||||
└─ tool-schemas/
|
||||
└─ {categoryKey}/
|
||||
├─ claim-search-resource-input-schema.json
|
||||
└─ claim-search-resource-output-schema.json
|
||||
```
|
||||
|
||||
예를 들어 `categoryKey`가 `cmm`이면 아래와 같이 선언합니다.
|
||||
|
||||
```java
|
||||
@McpFunction(
|
||||
name = "secret_tool",
|
||||
visible = false, // 목록 숨김 여부 (기본값: true)
|
||||
register = false // 내부 Redis 등록 여부 (기본값: true)
|
||||
name = "oth.cmm.claim.search",
|
||||
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
|
||||
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json"
|
||||
)
|
||||
public ClaimSearchResponse search(ClaimSearchRequest request) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
`inputSchemaResource`와 `outputSchemaResource`는 복잡한 경우에만 선언합니다. 단순한 Tool까지 JSON 파일을 별도 생성할 필요는 없습니다.
|
||||
|
||||
## 🛡️ 시스템 안정성 및 네트워크 제어 (Resilience & Network)
|
||||
### Output 설계 규칙
|
||||
|
||||
MSA 및 외부 시스템(MCI) 연동 환경의 안정성을 위해 완벽한 3-Tier 방어 체계를 구축했습니다.
|
||||
- 코드와 표시용 라벨을 함께 반환합니다. 예: `status` + `statusLabel`
|
||||
- `null`이 정상인 값은 의미를 설명에 명시하고 DTO에는 `nullable = true`를 설정합니다.
|
||||
- 조건부 필드는 어떤 조건에서 값이 존재하는지 JSON Schema에 명시합니다.
|
||||
- 배열은 정렬 기준을 설명에 명시합니다. 예: `접수일 내림차순`
|
||||
- 목록 응답에는 추가 조회 여부를 나타내는 `hasMore`를 포함합니다.
|
||||
- 민감정보는 마스킹보다 **응답에서 제외**하는 것을 우선합니다.
|
||||
|
||||
1. **Gateway 라우팅 방어 (Timeout & Fallback):**
|
||||
- MCP 라우터(`McpRouterController`) 단에 1초 타임아웃을 강제 적용하여 특정 Tool Pod의 응답 지연이 전체 시스템 장애로 이어지는 것을 방지하고 신속하게 정적 Fallback 라우팅으로 전환합니다.
|
||||
2. **MCI 네트워크 안정화 (HTTP/1.1 Downgrade):**
|
||||
- 기존 HTTP/2 사용 시 레거시 시스템 연동 중 간헐적으로 발생하던 `RST_STREAM` 오류를 원천 차단하기 위해, MCI 전용 `HttpEimsSender`에는 고도로 최적화된 **HTTP/1.1 전용 커넥션 풀(Factory)**이 고정 적용되어 네트워크 단절을 방지합니다.
|
||||
3. **Resilience4j 기반 트래픽 제어:**
|
||||
- **Gateway 계층 (동적 방어):** Tool 등록 시 제출된 SLA 메타데이터를 기반으로 동적 CircuitBreaker 및 RateLimiter를 가동하며, 한계치 초과 시 Kafka 큐로 비동기 전환합니다.
|
||||
- **Tool 계층 (정적 방어):** 레거시 커넥터 내부에 `@CircuitBreaker`, `@RateLimiter` 어노테이션 기반의 장애 전파 차단 로직이 2차적으로 가동됩니다.
|
||||
### 실행 로그 및 확인
|
||||
|
||||
---
|
||||
|
||||
## 모듈(Pod) 및 Tool 코드 자동 생성 (Scaffolders)
|
||||
|
||||
새로운 도메인의 기능을 추가할 때 발생하는 반복적인 설정(보일러플레이트, 설정 파일 복사 등)을 1초 만에 자동화하기 위해 **DAP Developer Portal (Web UI)** 및 **CLI 스캐폴더 2종**을 제공합니다.
|
||||
|
||||
### 1. DAP Developer Portal (Web UI) - 가장 추천하는 방식!
|
||||
이제 더 이상 터미널에서 명령어를 칠 필요가 없습니다. Gateway 모듈에 내장된 웹 화면에서 빈칸만 채우면 코드가 마법처럼 찍혀 나옵니다.
|
||||
|
||||
1. **접속 방법**: Gateway 서버 기동 후 브라우저에서 `http://localhost:8081/admin/scaffold.html` 접속
|
||||
2. **Pod (모듈) 생성 탭**: 모듈명(예: hr)과 포트만 입력하면 독립적인 Spring Boot 모듈이 디렉토리부터 빌드 스크립트까지 완벽히 생성됩니다.
|
||||
3. **Tool (기능) 생성 탭**: 생성된 모듈에 새로운 툴(서비스/DTO) 코드를 자동으로 주입합니다.
|
||||
|
||||
### 2. CLI 스캐폴더 (기존 터미널 방식)
|
||||
웹 화면을 사용할 수 없는 환경이거나 터미널이 익숙한 경우, 아래 명령어를 통해 CLI 마법사를 사용할 수 있습니다.
|
||||
|
||||
### 1⃣ 새로운 Pod(모듈) 전체를 생성할 때: `PodScaffolder`
|
||||
새로운 도메인(예: 결제, HR)을 위한 완전히 독립적인 Spring Boot 모듈을 생성합니다. 폴더 구조, 빌드 스크립트, 각종 프로퍼티 및 도커 설정까지 완벽하게 세팅됩니다.
|
||||
|
||||
```bash
|
||||
# 사용법: javac로 컴파일 후 실행
|
||||
javac -encoding UTF-8 dap-common/src/main/java/io/shinhanlife/dap/common/util/PodScaffolder.java
|
||||
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.PodScaffolder [모듈명] [포트번호]
|
||||
|
||||
# 실행 예시 (dap-tool-hr 모듈을 8086 포트로 생성)
|
||||
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.PodScaffolder hr 8086
|
||||
```
|
||||
|
||||
### 2⃣ 생성된 모듈에 새로운 툴(Function)을 추가할 때: `ToolScaffolder`
|
||||
어노테이션(`@McpTool`, `@McpFunction`)이 완벽히 달린 Service와 입출력 DTO 코드를 지정된 모듈 패키지 룰에 맞춰 자동 생성합니다.
|
||||
|
||||
```bash
|
||||
# 사용법: javac로 컴파일 후 실행
|
||||
javac -encoding UTF-8 dap-common/src/main/java/io/shinhanlife/dap/common/util/ToolScaffolder.java
|
||||
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.ToolScaffolder [Tool이름] [인터페이스ID] "[기능설명]" "[그룹명]" "[통신방식]" "[모듈명]"
|
||||
|
||||
# 실행 예시 (payment 모듈에 결제 승인 기능 추가)
|
||||
java -cp dap-common/src/main/java io.shinhanlife.dap.common.util.ToolScaffolder PaymentApproval PAY_001 "결제 승인 처리 기능" "COMMON" "HTTP" "dap-tool-payment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 패키지 구조 (Package Structure)
|
||||
Tool 실행이 끝나면 아래 로그는 Schema 정의가 아니라 **검증을 통과한 실제 최종 응답값**을 출력합니다.
|
||||
|
||||
```text
|
||||
dap-backend-main (Root)
|
||||
├── dap-gateway # MCP 라우팅 허브 서버 (외부 LLM과 통신 및 Tool 분배)
|
||||
├── dap-common # 공통 모듈 (Security, Session, Config 등)
|
||||
├── dap-tool-core # Tool 공통 기능 (AbstractMcpToolService, Annotation, Scaffolder)
|
||||
├── dap-tool-email # [Tool] 이메일 발송 특화 어댑터 모듈
|
||||
├── dap-tool-sms # [Tool] SMS 발송 특화 어댑터 모듈
|
||||
├── dap-tool-payment # [Tool] 결제 비즈니스 어댑터 모듈 (Scaffolded)
|
||||
└── dap-tool-other # [Tool] 기타 비즈니스(청구, 계약, 고객, HR 등) 어댑터 모듈
|
||||
[Tool -> MCP Gateway] Output Schema Result: { ... }
|
||||
```
|
||||
|
||||
*(참고: 기존 단일 모듈 프로젝트에서 마이크로서비스 확장을 위해 모듈별로 분리되었으며, 각 Tool 서버는 독립적으로 확장 및 배포할 수 있습니다.)*
|
||||
따라서 로그에도 실제 응답이 남으므로, 응답 DTO와 Output Schema에 민감정보가 포함되지 않도록 설계해야 합니다.
|
||||
|
||||
스키마 리소스와 DTO 기반 자동 Schema는 아래 테스트로 함께 검증할 수 있습니다.
|
||||
|
||||
```powershell
|
||||
.\gradlew.bat :dap-tool-oth:test --tests "io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequestSchemaTest"
|
||||
```
|
||||
|
||||
### Tool Naming Convention
|
||||
|
||||
All Tool names use the four-level lowercase format `pod.domain.service.action`. Do not use underscores or CamelCase; use a hyphen (`-`) only when a single level has multiple words.
|
||||
|
||||
- `pod`: deployment Tool Pod/module (`dap-tool-oth` → `oth`, `dap-tool-sms` → `sms`)
|
||||
- `domain`: business-domain package (`cmm`, `smp`, `sol`, etc.)
|
||||
- `service`: business service or resource
|
||||
- `action`: the requested operation (`search`, `list`, `detail`, `issue`, `inquiry`, etc.)
|
||||
|
||||
```text
|
||||
oth.cmm.bond.issue
|
||||
oth.cmm.claim.search
|
||||
oth.sol.request.list
|
||||
oth.smp.weather.inquiry
|
||||
```
|
||||
|
||||
When Scaffold receives `dap-tool-oth`, `cmm`, and `ClaimSearch`, it generates `oth.cmm.claim.search`. The `validateMcpToolNames` Gradle task rejects both a duplicate name and any name outside this format before packaging, including its source file and line number.
|
||||
|
||||
### Tool Test Console
|
||||
|
||||
각 Tool Pod는 공통 테스트 화면을 제공합니다.
|
||||
|
||||
```text
|
||||
http://localhost:8084/tool-test-console.html
|
||||
```
|
||||
|
||||
화면은 현재 Pod의 `/tool-manifest`에서 Tool 목록과 `inputSchema`를 읽습니다. Tool을 선택한 뒤 `Schema 샘플 채우기`로 요청 JSON을 만들고 실행할 수 있습니다. 업무에 맞게 보정한 요청은 `현재 요청 저장`으로 브라우저의 `localStorage`에 보관합니다.
|
||||
|
||||
`Run saved cases`는 저장된 테스트 케이스를 순차 실행해 성공/실패, HTTP 상태, 소요 시간을 보여줍니다. 따라서 Tool이 수백 개여도 각 Tool마다 테스트 화면을 만들 필요 없이, 유효한 업무 테스트 데이터만 한 번 저장하면 이후에는 몇 번의 클릭으로 회귀 테스트할 수 있습니다.
|
||||
|
||||
- Tool 호출은 현재 Pod의 `/mcp/{toolName}`로 수행합니다.
|
||||
- 매 실행마다 `trace-id`, `request-id`를 새로 생성하여 응답과 함께 표시합니다.
|
||||
- 외부 MCI/EAI Tool은 샘플값 대신 개발계에서 허용된 테스트 데이터를 저장해서 사용해야 합니다.
|
||||
|
||||
17
build.gradle
17
build.gradle
@@ -49,3 +49,20 @@ subprojects {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
|
||||
def toolCoreProject = project(':dap-was-lib')
|
||||
|
||||
tasks.register('validateMcpToolNames', JavaExec) {
|
||||
group = 'verification'
|
||||
description = 'Checks duplicate @McpFunction names across all Tool modules before packaging.'
|
||||
dependsOn toolCoreProject.tasks.named('classes')
|
||||
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
|
||||
mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner')
|
||||
args rootProject.projectDir.absolutePath
|
||||
}
|
||||
|
||||
subprojects {
|
||||
tasks.matching { it.name == 'bootJar' }.configureEach {
|
||||
dependsOn rootProject.tasks.named('validateMcpToolNames')
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
|
||||
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.dto
|
||||
* @className ToolMetadata
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
public class ToolMetadata {
|
||||
|
||||
// 1. Tool 기본 정보
|
||||
private String uid; // UUID 형식의 고유 식별자
|
||||
private String semver; // 버전 (예: 1.0.0)
|
||||
private String displayName; // 사람이 읽는 라벨 (1-128자)
|
||||
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
|
||||
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
|
||||
|
||||
// 2. 파라미터 스키마 (JSON Schema 형태의 Map)
|
||||
private Map<String, Object> parametersSchema;
|
||||
|
||||
// 2-0. 프론트엔드 UI용 함수별 프롬프트 매핑 (추가됨)
|
||||
private Map<String, String> actionPrompts;
|
||||
|
||||
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
|
||||
private String categoryKey;
|
||||
|
||||
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
|
||||
private String endpoint;
|
||||
|
||||
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
|
||||
private String podUrl;
|
||||
|
||||
// 2-4. 가시성 여부
|
||||
@Builder.Default
|
||||
private Boolean visible = true;
|
||||
|
||||
// 2-5. Redis 등록 여부 (UI 표출용)
|
||||
@Builder.Default
|
||||
private Boolean isRegistered = true;
|
||||
|
||||
// 2-6. HITL 승인 필요 여부
|
||||
@Builder.Default
|
||||
private Boolean requiresApproval = false;
|
||||
|
||||
|
||||
|
||||
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
|
||||
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
|
||||
|
||||
// 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용)
|
||||
private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992)
|
||||
|
||||
// 5. 인프라 상태 정보 (DIRECT 연동 시 사용)
|
||||
private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프
|
||||
|
||||
// 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반)
|
||||
private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%)
|
||||
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
|
||||
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package io.shinhanlife.dap.common.integration.mci.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.common.integration.mci.dto
|
||||
* @className ShinhanCommonHeaderDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ShinhanCommonHeaderDto {
|
||||
|
||||
private String envrTypeCd; // 환경유형코드 (D, T, R)
|
||||
private String glbId; // 전사공통키 (37 Byte)
|
||||
private String pgrsSriaNo; // 진행일련번호
|
||||
private String trgmVrsnInfoValu; // 전문버전정보값
|
||||
private String tgrmEncrYn; // 전문암호화여부
|
||||
private String gpcpCd; // 글로벌법인코드
|
||||
private String appliDutjCd; // 어플리케이션업무코드
|
||||
private String rcvSvcId; // 수신서비스ID
|
||||
private String reqRspnScCd; // 요청응답구분코드 (S:요청, R:응답)
|
||||
private String inqrTraTypeCd; // 조회거래유형코드
|
||||
private String reqTgrmTnsmDtptDt; // 요청전문전송일시
|
||||
private String itrIfId; // 인터페이스ID
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
package io.shinhanlife.dap.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Scanner;
|
||||
|
||||
/**
|
||||
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
|
||||
*
|
||||
* [실행 방법]
|
||||
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
|
||||
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
|
||||
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
|
||||
*
|
||||
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
|
||||
* - 컴파일: javac -encoding UTF-8 dap-tool-core/src/main/java/io/shinhanlife/dap/biz/mcp/tool/util/ToolScaffolder.java
|
||||
* - 실행: java -cp dap-tool-core/src/main/java io.shinhanlife.dap.biz.mcp.tool.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
|
||||
*/
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.tool.util
|
||||
* @className ToolScaffolder
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public class ToolScaffolder {
|
||||
|
||||
private static final String BASE_PACKAGE = "io.shinhanlife.dap.biz.mcp.tool";
|
||||
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/biz/mcp/tool";
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
System.out.println("=========================================");
|
||||
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
|
||||
System.out.println("=========================================\n");
|
||||
|
||||
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
|
||||
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
|
||||
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): ");
|
||||
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
|
||||
if (group.isEmpty()) group = "COMMON";
|
||||
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
|
||||
if (routingType.trim().isEmpty()) {
|
||||
routingType = "HTTP";
|
||||
}
|
||||
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-tool-other): ");
|
||||
if (moduleName.trim().isEmpty()) {
|
||||
moduleName = "dap-tool-other";
|
||||
}
|
||||
|
||||
String defaultAuthor = System.getProperty("user.name");
|
||||
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||
|
||||
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
|
||||
if (author.trim().isEmpty()) author = defaultAuthor;
|
||||
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
|
||||
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||
|
||||
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
|
||||
if (args.length > index) {
|
||||
return args[index];
|
||||
}
|
||||
System.out.print(prompt);
|
||||
return scanner.nextLine().trim();
|
||||
}
|
||||
|
||||
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register) throws IOException {
|
||||
baseName = toPascalCase(baseName);
|
||||
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
|
||||
|
||||
Path serviceDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "service"));
|
||||
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "dto"));
|
||||
|
||||
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
|
||||
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "dto"));
|
||||
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, shortName, "converter"));
|
||||
|
||||
Files.createDirectories(serviceDir);
|
||||
Files.createDirectories(dtoDir);
|
||||
Files.createDirectories(legacyDtoDir);
|
||||
Files.createDirectories(converterDir);
|
||||
|
||||
StringBuilder log = new StringBuilder();
|
||||
|
||||
// Generate Req DTO
|
||||
String reqContent = """
|
||||
package %s.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sReq
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %sReq {
|
||||
// TODO: Add request fields here
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Req.java"), reqContent);
|
||||
|
||||
// Generate Res DTO
|
||||
String resContent = """
|
||||
package %s.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.dto
|
||||
* @className %sRes
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %sRes {
|
||||
private String status;
|
||||
private String message;
|
||||
// TODO: Add response fields here
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, BASE_PACKAGE, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(dtoDir.resolve(baseName + "Res.java"), resContent);
|
||||
|
||||
String toolName = baseName.isEmpty() ? baseName : Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1);
|
||||
|
||||
// Generate Service
|
||||
String serviceContent = """
|
||||
package %s.service;
|
||||
|
||||
import %s.annotation.McpFunction;
|
||||
import %s.annotation.McpTool;
|
||||
import %s.dto.%sReq;
|
||||
import %s.dto.%sRes;
|
||||
import %s.%s.converter.%sLegacyConverter;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* @package %s.service
|
||||
* @className %sService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
@McpTool(
|
||||
routingType = "%s",
|
||||
categoryKey = "%s"
|
||||
)
|
||||
public class %sService extends AbstractMcpToolService {
|
||||
|
||||
private final %sLegacyConverter converter = Mappers.getMapper(%sLegacyConverter.class);
|
||||
|
||||
@McpFunction(
|
||||
displayName = "%s 툴",
|
||||
name = "%s",
|
||||
description = "%s",
|
||||
prompt = "%s",
|
||||
mappingId = "%s",
|
||||
register = %s,
|
||||
requiresApproval = false
|
||||
)
|
||||
public Object execute(%sReq req) {
|
||||
// %sLegacyReq legacyReq = converter.toLegacyReq(req);
|
||||
return executeLegacy("%s", "%s", req); // Or pass legacyReq
|
||||
}
|
||||
}
|
||||
""".formatted(
|
||||
BASE_PACKAGE,
|
||||
BASE_PACKAGE,
|
||||
BASE_PACKAGE,
|
||||
BASE_PACKAGE, baseName,
|
||||
BASE_PACKAGE, baseName,
|
||||
BASE_PACKAGE, shortName, baseName,
|
||||
BASE_PACKAGE,
|
||||
baseName,
|
||||
author,
|
||||
createDate,
|
||||
createDate, author,
|
||||
routingType, group.toLowerCase(),
|
||||
baseName,
|
||||
baseName, baseName,
|
||||
baseName, toolName, description, description + " 해줘.", interfaceId, register,
|
||||
baseName,
|
||||
baseName,
|
||||
routingType, interfaceId
|
||||
);
|
||||
|
||||
Files.writeString(serviceDir.resolve(baseName + "Service.java"), serviceContent);
|
||||
|
||||
|
||||
|
||||
// Generate Legacy Req DTO
|
||||
String legacyReqContent = """
|
||||
package %s.%s.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.%s.dto
|
||||
* @className %sLegacyReq
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %sLegacyReq {
|
||||
// TODO: Add legacy request fields here
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyReq.java"), legacyReqContent);
|
||||
|
||||
// Generate Legacy Res DTO
|
||||
String legacyResContent = """
|
||||
package %s.%s.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @package %s.%s.dto
|
||||
* @className %sLegacyRes
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Data
|
||||
public class %sLegacyRes {
|
||||
// TODO: Add legacy response fields here
|
||||
}
|
||||
""".formatted(BASE_PACKAGE, shortName, BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author, baseName);
|
||||
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRes.java"), legacyResContent);
|
||||
|
||||
// Generate Legacy Converter
|
||||
String converterContent = """
|
||||
package %s.%s.converter;
|
||||
|
||||
import %s.dto.%sReq;
|
||||
import %s.dto.%sRes;
|
||||
import %s.%s.dto.%sLegacyReq;
|
||||
import %s.%s.dto.%sLegacyRes;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* @package %s.%s.converter
|
||||
* @className %sLegacyConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author %s
|
||||
* @create %s
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* %s %s 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface %sLegacyConverter {
|
||||
|
||||
// @Mapping(source = "sourceField", target = "targetField")
|
||||
%sLegacyReq toLegacyReq(%sReq req);
|
||||
|
||||
%sRes toRes(%sLegacyRes legacyRes);
|
||||
}
|
||||
""".formatted(
|
||||
BASE_PACKAGE, shortName,
|
||||
BASE_PACKAGE, baseName,
|
||||
BASE_PACKAGE, baseName,
|
||||
BASE_PACKAGE, shortName, baseName,
|
||||
BASE_PACKAGE, shortName, baseName,
|
||||
BASE_PACKAGE, shortName, baseName, author, createDate, createDate, author,
|
||||
baseName, baseName, baseName, baseName, baseName, baseName
|
||||
);
|
||||
Files.writeString(converterDir.resolve(baseName + "LegacyConverter.java"), converterContent);
|
||||
|
||||
log.append("\n=========================================\n");
|
||||
log.append(" Scaffolding Complete!\n");
|
||||
log.append("=========================================\n");
|
||||
log.append("[Service] ").append(serviceDir.resolve(baseName + "Service.java")).append("\n");
|
||||
log.append("[Req DTO] ").append(dtoDir.resolve(baseName + "Req.java")).append("\n");
|
||||
log.append("[Res DTO] ").append(dtoDir.resolve(baseName + "Res.java")).append("\n");
|
||||
log.append("[Legacy Req DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyReq.java")).append("\n");
|
||||
log.append("[Legacy Res DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRes.java")).append("\n");
|
||||
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "LegacyConverter.java")).append("\n");
|
||||
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
|
||||
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
private static String toPascalCase(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return str;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean capitalizeNext = true;
|
||||
for (char c : str.toCharArray()) {
|
||||
if (c == '_' || c == '-' || c == ' ') {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result.append(Character.toUpperCase(c));
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result.append(c);
|
||||
}
|
||||
}
|
||||
if (result.length() > 0) {
|
||||
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package io.shinhanlife.glow;
|
||||
|
||||
public @interface GlowControllerId {
|
||||
String value();
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package io.shinhanlife.glow;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface GlowIndexPaging {
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package io.shinhanlife.glow;
|
||||
|
||||
public @interface GlowServiceGroupId {
|
||||
String value();
|
||||
|
||||
String description();
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.shinhanlife.glow;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface GlowTrgmField {
|
||||
|
||||
/**
|
||||
* 필드 순서
|
||||
*/
|
||||
int order();
|
||||
|
||||
/**
|
||||
* 필드 길이
|
||||
*/
|
||||
int length();
|
||||
|
||||
/**
|
||||
* 필드 설명
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
}
|
||||
@@ -1,17 +1,8 @@
|
||||
FROM eclipse-temurin:21-jdk-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY gradlew .
|
||||
COPY gradle gradle
|
||||
COPY build.gradle settings.gradle ./
|
||||
COPY dap-common dap-common
|
||||
COPY dap-gateway dap-gateway
|
||||
RUN chmod +x gradlew
|
||||
RUN ./gradlew :dap-gateway:build -x test
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ=Asia/Seoul
|
||||
COPY --from=builder /app/dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
|
||||
COPY dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
|
||||
EXPOSE 8081
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':dap-common')
|
||||
implementation project(':dap-was-lib')
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
@@ -11,6 +11,7 @@ dependencies {
|
||||
// Spring AI MCP Server
|
||||
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
||||
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
|
||||
implementation 'org.springframework.ai:spring-ai-openai:2.0.0'
|
||||
// MyBatis & DB
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package io.shinhanlife.dap.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.dap.biz.mcp.gateway", "io.shinhanlife.dap.common.mcp", "io.shinhanlife.dap.common.config"})
|
||||
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.biz.mcp.gateway", "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);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Audit Log, Redis Trace, Agent 응답 preview에 남으면 안 되는 민감정보를 마스킹합니다.
|
||||
*/
|
||||
@Component
|
||||
public class SensitiveDataMasker {
|
||||
private static final Set<String> SENSITIVE_KEYS = Set.of(
|
||||
"password", "passwd", "pwd", "token", "accessToken", "refreshToken", "secret",
|
||||
"ssn", "rrn", "residentNumber", "cardNumber", "accountNumber", "accountNo",
|
||||
"phone", "mobile", "email", "idempotencyKey");
|
||||
private static final Pattern EMAIL = Pattern.compile("([a-zA-Z0-9._%+-]{2})[a-zA-Z0-9._%+-]*(@[a-zA-Z0-9.-]+)");
|
||||
private static final Pattern CARD_OR_ACCOUNT = Pattern.compile("\\b(\\d{4})\\d{4,12}(\\d{2,4})\\b");
|
||||
private final ObjectMapper json;
|
||||
|
||||
public SensitiveDataMasker(ObjectMapper json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
/**
|
||||
* JsonNode 전체를 재귀적으로 순회하며 민감 key와 민감 패턴을 마스킹합니다.
|
||||
*/
|
||||
public JsonNode mask(JsonNode input) {
|
||||
if (input == null || input.isMissingNode() || input.isNull()) {
|
||||
return json.createObjectNode();
|
||||
}
|
||||
if (input.isArray()) {
|
||||
ArrayNode masked = json.createArrayNode();
|
||||
for (JsonNode item : input) {
|
||||
masked.add(mask(item));
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
if (input.isObject()) {
|
||||
ObjectNode masked = json.createObjectNode();
|
||||
Iterator<Map.Entry<String, JsonNode>> fields = input.fields();
|
||||
while (fields.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = fields.next();
|
||||
String key = entry.getKey();
|
||||
JsonNode value = entry.getValue();
|
||||
if (isSensitiveKey(key)) {
|
||||
masked.put(key, "***");
|
||||
} else {
|
||||
masked.set(key, mask(value));
|
||||
}
|
||||
}
|
||||
return masked;
|
||||
}
|
||||
if (input.isTextual()) {
|
||||
return json.valueToTree(maskText(input.asText()));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
private boolean isSensitiveKey(String key) {
|
||||
return key != null && SENSITIVE_KEYS.stream().anyMatch(sensitive -> sensitive.equalsIgnoreCase(key));
|
||||
}
|
||||
|
||||
private String maskText(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return value;
|
||||
}
|
||||
String masked = EMAIL.matcher(value).replaceAll("$1***$2");
|
||||
return CARD_OR_ACCOUNT.matcher(masked).replaceAll("$1********$2");
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
|
||||
/**
|
||||
* Circuit Breaker의 현재 상태입니다.
|
||||
*
|
||||
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
|
||||
*/
|
||||
public enum CircuitBreakerState {
|
||||
CLOSED,
|
||||
OPEN,
|
||||
HALF_OPEN
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.transport;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpToolInvoker implements ToolInvoker {
|
||||
|
||||
private final RestClient restClient;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public HttpToolInvoker(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.restClient = RestClient.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode invoke(Map<String, Object> payload, String targetUrl) {
|
||||
try {
|
||||
Object httpResult = restClient.post()
|
||||
.uri(targetUrl)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
// TODO: Use actual tenant's key
|
||||
.header("X-Trace-Id", UUID.randomUUID().toString())
|
||||
.body(payload)
|
||||
.retrieve()
|
||||
.body(Object.class);
|
||||
|
||||
return extractData(objectMapper.valueToTree(httpResult));
|
||||
} catch (Exception e) {
|
||||
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod HTTP 호출 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode extractData(JsonNode root) {
|
||||
if (!root.path("success").asBoolean(true)) {
|
||||
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool 서버 업무 오류: " + root.path("error").asText());
|
||||
}
|
||||
return root.has("data") ? root.get("data") : root;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package io.shinhanlife.dap.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);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.converter;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.domain.repository;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleKnwlAthrInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleToolAthrInDto;
|
||||
import io.shinhanlife.glow.GlowMybatisMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.domain.service;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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);
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.domain.service.impl;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.domain.repository.AccessMgmtMapper;
|
||||
import io.shinhanlife.dap.biz.so.atm.domain.service.AccessMgmtService;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSaveInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrSearchInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleKnwlAthrInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListInDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleToolAthrInDto;
|
||||
import io.shinhanlife.dap.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.dap.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();
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.dto;
|
||||
|
||||
import lombok.Setter;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.so.atm.dto
|
||||
* @className AthrOutDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
public class AthrOutDto {
|
||||
private List<AthrItemOutDto> tools;
|
||||
private List<AthrItemOutDto> knwls;
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package io.shinhanlife.dap.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.dap.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;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package io.shinhanlife.dap.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.dap.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;
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.so.atm.dto
|
||||
* @className RoleListOutDto
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RoleListOutDto {
|
||||
private String systId;
|
||||
private String roleNo;
|
||||
private String roleNm;
|
||||
private String roleDs;
|
||||
private String puseYn;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package io.shinhanlife.dap.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.dap.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;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.presentation;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
|
||||
import io.shinhanlife.dap.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.dap.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();
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.presentation.io;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.presentation.io;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.so.atm.presentation.io
|
||||
* @className AthrSearchRequest
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
public class AthrSearchRequest {
|
||||
private String systId;
|
||||
private String roleNo;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.presentation.io;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.AthrItemOutDto;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.presentation.io;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.usecase;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package io.shinhanlife.dap.biz.so.atm.usecase.impl;
|
||||
|
||||
import io.shinhanlife.dap.biz.so.atm.converter.AccessMgmtConverter;
|
||||
import io.shinhanlife.dap.biz.so.atm.domain.service.AccessMgmtService;
|
||||
import io.shinhanlife.dap.biz.so.atm.dto.RoleListOutDto;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSaveRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.AthrSearchResponse;
|
||||
import io.shinhanlife.dap.biz.so.atm.presentation.io.RoleListRequest;
|
||||
import io.shinhanlife.dap.biz.so.atm.usecase.AccessMgmtUseCase;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package io.shinhanlife.dap.mcg;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg
|
||||
* @className DapGatewayApplication
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </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;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
|
||||
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcg", "io.shinhanlife.dap.lib.mcp", "io.shinhanlife.dap.lib.config"})
|
||||
@EnableCaching
|
||||
@EnableScheduling
|
||||
public class DapGatewayApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DapGatewayApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.aop;
|
||||
package io.shinhanlife.dap.mcg.aop;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
@@ -8,16 +8,16 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.aop
|
||||
* @package io.shinhanlife.dap.mcg.aop
|
||||
* @className GatewayLoggingAspect
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -27,7 +27,7 @@ import org.springframework.util.StopWatch;
|
||||
public class GatewayLoggingAspect {
|
||||
|
||||
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 시 작동
|
||||
@Around("execution(* io.shinhanlife.dap.biz.mcp.gateway.presentation..*(..))")
|
||||
@Around("execution(* io.shinhanlife.dap.mcg.presentation..*(..))")
|
||||
public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
String targetMethod = joinPoint.getSignature().toShortString();
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
@@ -1,8 +1,22 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.audit;
|
||||
package io.shinhanlife.dap.mcg.audit;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.audit
|
||||
* @className AuditLogService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -17,11 +31,8 @@ import org.springframework.stereotype.Service;
|
||||
public class AuditLogService {
|
||||
private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT");
|
||||
private final McpGatewayProperties properties;
|
||||
private final SensitiveDataMasker masker;
|
||||
|
||||
public AuditLogService(McpGatewayProperties properties, SensitiveDataMasker masker) {
|
||||
public AuditLogService(McpGatewayProperties properties) {
|
||||
this.properties = properties;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +43,7 @@ public class AuditLogService {
|
||||
return;
|
||||
}
|
||||
audit.info("event=tool_started requestId={} agentId={} userId={} clientAddress={} tool={} arguments={}",
|
||||
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, masker.mask(arguments));
|
||||
context.requestId(), context.agentId(), context.userId(), context.clientAddress(), toolName, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.config;
|
||||
package io.shinhanlife.dap.mcg.config;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.config
|
||||
* @className AgentResponseBudgetProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.config;
|
||||
package io.shinhanlife.dap.mcg.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -8,16 +8,16 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.config
|
||||
* @package io.shinhanlife.dap.mcg.config
|
||||
* @className GatewayFallbackProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.config;
|
||||
package io.shinhanlife.dap.mcg.config;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.config
|
||||
* @className McpGatewayProperties
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -10,7 +25,7 @@ import java.util.stream.Collectors;
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "mcp")
|
||||
/**
|
||||
* application.properties의 mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다.
|
||||
* application.yml의 mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다.
|
||||
*
|
||||
* API Key, 허용 Tool, 감사 로그, Redis Trace, Retry, Circuit Breaker,
|
||||
* EIMS/Tool 서버 주소 같은 MCP Gateway 운영 설정을 한 곳에서 관리합니다.
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.config;
|
||||
package io.shinhanlife.dap.mcg.config;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
@@ -10,16 +10,16 @@ import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.config
|
||||
* @package io.shinhanlife.dap.mcg.config
|
||||
* @className RedisConfig
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,8 +1,23 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
|
||||
package io.shinhanlife.dap.mcg.guardrail;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.guardrail
|
||||
* @className GuardrailService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -1,8 +1,23 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.guardrail;
|
||||
package io.shinhanlife.dap.mcg.guardrail;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.guardrail
|
||||
* @className ToolResponseGuardrailService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.messaging;
|
||||
package io.shinhanlife.dap.mcg.messaging;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -10,16 +10,16 @@ import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.messaging
|
||||
* @package io.shinhanlife.dap.mcg.messaging
|
||||
* @className KafkaProducerService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,8 +1,24 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
|
||||
package io.shinhanlife.dap.mcg.presentation;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.presentation
|
||||
* @className ChatController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
@@ -11,6 +27,7 @@ import org.springframework.ai.tool.definition.ToolDefinition;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@@ -26,6 +43,7 @@ public class ChatController {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RedisRegistryService registryService;
|
||||
private final ChatClient.Builder chatClientBuilder;
|
||||
private final McpRouterController mcpRouterController;
|
||||
|
||||
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter chatStream(@RequestBody Map<String, String> request,
|
||||
@@ -39,19 +57,45 @@ public class ChatController {
|
||||
|
||||
try {
|
||||
List<ToolCallback> callbacks = new ArrayList<>();
|
||||
for (ToolMetadata meta : registryService.getAllTools()) {
|
||||
if (Boolean.TRUE.equals(meta.getVisible())) {
|
||||
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
|
||||
JsonRpcResponse toolsResponse = mcpRouterController.listTools(null).getBody();
|
||||
if (toolsResponse != null && toolsResponse.getResult() instanceof Map) {
|
||||
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
|
||||
if (resultMap.containsKey("tools")) {
|
||||
List<ToolMetadata> allTools = (List<ToolMetadata>) resultMap.get("tools");
|
||||
Set<String> addedToolNames = new HashSet<>();
|
||||
for (ToolMetadata meta : allTools) {
|
||||
if (Boolean.TRUE.equals(meta.getVisible()) && meta.getName() != null) {
|
||||
String cleanName = meta.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
|
||||
if (!addedToolNames.contains(cleanName)) {
|
||||
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
|
||||
addedToolNames.add(cleanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChatClient chatClient = chatClientBuilder
|
||||
String selectedModel = request.getOrDefault("model", "gemini-flash-latest").trim();
|
||||
if (selectedModel.isEmpty()) {
|
||||
selectedModel = "gemini-flash-latest";
|
||||
}
|
||||
|
||||
// 1. gemini-flash-latest 선택 시 OpenRouter의 Google Gemma 4 모델로 라우팅
|
||||
if (selectedModel.equals("gemini-flash-latest")) {
|
||||
selectedModel = "google/gemma-4-31b-it:free";
|
||||
log.info("[Real AI Chat] Gemini 직접 API → OpenRouter Google Gemma 4 31B 로 라우팅 전환");
|
||||
}
|
||||
|
||||
// 2. OpenRouter 무료 모델 처리 (Spring AI 빌드된 ChatClient 및 MCP 툴 호출 완벽 지원!)
|
||||
ChatClient activeChatClient = chatClientBuilder
|
||||
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
|
||||
.build();
|
||||
|
||||
Flux<String> responseStream = chatClient.prompt()
|
||||
Flux<String> responseStream = activeChatClient.prompt()
|
||||
.user(message)
|
||||
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools()
|
||||
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
|
||||
.model(selectedModel))
|
||||
.stream()
|
||||
.content();
|
||||
|
||||
@@ -74,7 +118,7 @@ public class ChatController {
|
||||
} else if (errorMsg != null && errorMsg.contains("503")) {
|
||||
emitter.send("\n\n⚠️ **AI 모델 서버 혼잡 (503)**: 현재 AI 모델을 제공하는 서버에 일시적으로 접속자가 많아 지연이 발생하고 있습니다. 잠시 후 다시 시도해 주세요.");
|
||||
} else if (errorMsg != null && errorMsg.contains("429")) {
|
||||
emitter.send("\n\n⚠️ **API 사용량 초과 (429)**: 현재 사용 중인 Gemini API(무료 티어)의 일일 또는 분당 요청 한도를 초과했습니다. 잠시 후 다시 시도하시거나 API 플랜을 확인해 주세요.");
|
||||
emitter.send("\n\n⚠️ **API 사용량 초과 (429)**: AI 모델 API(무료 티어 또는 오픈라우터)의 요청 한도를 초과했습니다. 잠시 후 다시 시도하시거나 API 키의 유효 한도를 확인해 주세요.");
|
||||
} else {
|
||||
emitter.send("\n[에러 발생: " + errorMsg + "]");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
|
||||
package io.shinhanlife.dap.mcg.presentation;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.service.KillSwitchService;
|
||||
import io.shinhanlife.dap.mcg.service.KillSwitchService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -8,16 +8,16 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.presentation
|
||||
* @package io.shinhanlife.dap.mcg.presentation
|
||||
* @className KillSwitchController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,14 +1,29 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
|
||||
package io.shinhanlife.dap.mcg.presentation;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.presentation
|
||||
* @className McpRouterController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcRequest;
|
||||
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.biz.mcp.adapter.dto.Params;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
|
||||
import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
|
||||
import io.shinhanlife.dap.lib.adapter.dto.Params;
|
||||
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -76,7 +91,7 @@ public class McpRouterController {
|
||||
try {
|
||||
Object result = executeService.execute(payload, effectiveTenantId);
|
||||
|
||||
io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse response = new io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse();
|
||||
JsonRpcResponse response = new JsonRpcResponse();
|
||||
response.setJsonrpc("2.0");
|
||||
response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString());
|
||||
response.setResult(result);
|
||||
@@ -97,9 +112,7 @@ public class McpRouterController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/tools/list")
|
||||
public ResponseEntity<JsonRpcResponse> listTools(
|
||||
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
|
||||
private List<ToolMetadata> fetchAllActiveTools() {
|
||||
List<ToolMetadata> activeTools = redisRegistryService.getAllTools()
|
||||
.stream()
|
||||
.filter(ToolMetadata::getVisible)
|
||||
@@ -133,6 +146,14 @@ public class McpRouterController {
|
||||
log.warn("Failed to fetch local tools from fallback URL: {}", url);
|
||||
}
|
||||
}
|
||||
return activeTools;
|
||||
}
|
||||
|
||||
@GetMapping("/tools/list")
|
||||
public ResponseEntity<JsonRpcResponse> listTools(
|
||||
@RequestParam(value = "categoryKey", required = false) String categoryKey) {
|
||||
|
||||
List<ToolMetadata> activeTools = fetchAllActiveTools();
|
||||
|
||||
if (categoryKey != null && !categoryKey.trim().isEmpty()) {
|
||||
activeTools = activeTools.stream()
|
||||
@@ -146,12 +167,10 @@ public class McpRouterController {
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8")
|
||||
public ResponseEntity<String> generateToolsMarkdown() {
|
||||
List<ToolMetadata> tools = redisRegistryService.getAllTools()
|
||||
.stream()
|
||||
.filter(ToolMetadata::getVisible)
|
||||
.collect(Collectors.toList());
|
||||
List<ToolMetadata> tools = fetchAllActiveTools();
|
||||
|
||||
StringBuilder md = new StringBuilder();
|
||||
md.append("# \uD83E\uDD16 Shinhan AI Tool Catalog\n\n");
|
||||
@@ -1,8 +1,23 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
|
||||
package io.shinhanlife.dap.mcg.presentation;
|
||||
|
||||
import io.shinhanlife.dap.common.util.PodScaffolder;
|
||||
import io.shinhanlife.dap.common.util.ToolScaffolder;
|
||||
import io.shinhanlife.dap.common.util.ToolSourceUpdater;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.presentation
|
||||
* @className ScaffoldingController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.util.PodScaffolder;
|
||||
import io.shinhanlife.dap.lib.util.ToolScaffolder;
|
||||
import io.shinhanlife.dap.lib.util.ToolSourceUpdater;
|
||||
import java.io.File;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -19,7 +34,7 @@ public class ScaffoldingController {
|
||||
@PostMapping("/pod")
|
||||
public String scaffoldPod(@RequestBody Map<String, String> req) {
|
||||
try {
|
||||
String moduleName = req.getOrDefault("moduleName", "dap-tool-other");
|
||||
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
|
||||
if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName;
|
||||
String port = req.getOrDefault("port", "8085");
|
||||
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
|
||||
@@ -42,14 +57,15 @@ public class ScaffoldingController {
|
||||
String description = req.get("description");
|
||||
String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON"));
|
||||
String routingType = req.getOrDefault("routingType", "HTTP");
|
||||
String moduleName = req.getOrDefault("moduleName", "dap-tool-other");
|
||||
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
|
||||
String author = req.get("author");
|
||||
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
|
||||
String date = req.get("date");
|
||||
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
|
||||
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
|
||||
String clientSystemCode = req.get("clientSystemCode");
|
||||
|
||||
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register);
|
||||
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode);
|
||||
} catch (Exception e) {
|
||||
return "오류 발생: " + e.getMessage();
|
||||
}
|
||||
@@ -78,13 +94,15 @@ public class ScaffoldingController {
|
||||
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
|
||||
|
||||
File dir = new File(sourceDir);
|
||||
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-tool-core"));
|
||||
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-tool-") && !f.getName().equals("dap-was-lib"));
|
||||
|
||||
if (files == null) return List.of("dap-tool-other");
|
||||
if (files == null || files.length == 0) {
|
||||
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
|
||||
}
|
||||
|
||||
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
return List.of("dap-tool-other");
|
||||
return List.of("dap-was-oth", "dap-tool-hr", "dap-was-sms");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.redis;
|
||||
package io.shinhanlife.dap.mcg.redis;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.redis
|
||||
* @className McpMonitorEventService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.redis;
|
||||
package io.shinhanlife.dap.mcg.redis;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.redis
|
||||
* @className RedisToolTraceService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.mcg.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -34,19 +48,16 @@ public class RedisToolTraceService {
|
||||
private final ObjectProvider<StringRedisTemplate> redisProvider;
|
||||
private final ObjectMapper json;
|
||||
private final McpMonitorEventService monitorEvents;
|
||||
private final SensitiveDataMasker masker;
|
||||
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
|
||||
|
||||
public RedisToolTraceService(McpGatewayProperties properties,
|
||||
ObjectProvider<StringRedisTemplate> redisProvider,
|
||||
ObjectMapper json,
|
||||
McpMonitorEventService monitorEvents,
|
||||
SensitiveDataMasker masker) {
|
||||
McpMonitorEventService monitorEvents) {
|
||||
this.properties = properties;
|
||||
this.redisProvider = redisProvider;
|
||||
this.json = json;
|
||||
this.monitorEvents = monitorEvents;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +202,7 @@ public class RedisToolTraceService {
|
||||
arguments.fieldNames().forEachRemaining(argNames::add);
|
||||
trace.put("argumentNames", argNames);
|
||||
|
||||
trace.put("arguments", masker.mask(arguments).toString());
|
||||
trace.put("arguments", arguments.toString());
|
||||
trace.put("responseSummary", responseSummary(responseText));
|
||||
trace.put("timestamp", Instant.now().toString());
|
||||
return json.writeValueAsString(trace);
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.registry;
|
||||
package io.shinhanlife.dap.mcg.registry;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -13,16 +13,16 @@ import java.util.stream.Collectors;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.registry
|
||||
* @package io.shinhanlife.dap.mcg.registry
|
||||
* @className RedisRegistryService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className CircuitBreaker
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className CircuitBreakerMonitorController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -1,6 +1,21 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className CircuitBreakerService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Clock;
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className CircuitBreakerSnapshot
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
/**
|
||||
* MCP Monitor 화면에 내려주는 Circuit Breaker 상태 정보입니다.
|
||||
*/
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className CircuitBreakerState
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
/**
|
||||
* Circuit Breaker의 현재 상태입니다.
|
||||
*
|
||||
* CLOSED는 정상 호출 가능, OPEN은 호출 차단, HALF_OPEN은 복구 확인 상태를 의미합니다.
|
||||
*/
|
||||
public enum CircuitBreakerState {
|
||||
CLOSED,
|
||||
OPEN,
|
||||
HALF_OPEN
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className FailureType
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
/**
|
||||
* Tool/EIMS/Tool 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다.
|
||||
*
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className RetryPolicy
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
/**
|
||||
* Tool 호출 재시도 정책입니다.
|
||||
*
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.resilience;
|
||||
package io.shinhanlife.dap.mcg.resilience;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.resilience
|
||||
* @className ToolExecutionException
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
/**
|
||||
* Tool 실행 중 발생한 오류를 FailureType과 함께 전달하는 공통 예외입니다.
|
||||
*
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.security;
|
||||
package io.shinhanlife.dap.mcg.security;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.security
|
||||
* @className McpRequestContext
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.security;
|
||||
package io.shinhanlife.dap.mcg.security;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.security
|
||||
* @className McpRequestContextResolver
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
@@ -1,10 +1,25 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.security;
|
||||
package io.shinhanlife.dap.mcg.security;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.security
|
||||
* @className ToolAuthorizationService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </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.lib.dto.OperationType;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Set;
|
||||
@@ -1,20 +1,36 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.service;
|
||||
package io.shinhanlife.dap.mcg.service;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.RetryPolicy;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.GuardrailService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContext;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.security.McpRequestContextResolver;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.audit.AuditLogService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreaker;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreakerService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.security.ToolAuthorizationService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.redis.RedisToolTraceService;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.service
|
||||
* @className ExecuteService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.util.HashMap;
|
||||
|
||||
import io.shinhanlife.dap.lib.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.lib.dto.OperationType;
|
||||
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
|
||||
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 java.util.Map;
|
||||
@@ -27,12 +43,12 @@ import org.springframework.web.client.RestClient;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.LargeToolResponseService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.PaginationRequestValidator;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResultFormatter;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.ToolResponseGuardrailService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.transport.ToolInvoker;
|
||||
import io.shinhanlife.dap.mcg.tool.large.LargeToolResponseService;
|
||||
import io.shinhanlife.dap.mcg.tool.large.PaginationRequestValidator;
|
||||
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResultFormatter;
|
||||
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
|
||||
import io.shinhanlife.dap.mcg.guardrail.ToolResponseGuardrailService;
|
||||
import io.shinhanlife.dap.mcg.transport.ToolInvoker;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -41,7 +57,6 @@ public class ExecuteService {
|
||||
private final ToolPlanner planner;
|
||||
private final KillSwitchService killSwitchService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SensitiveDataMasker dataMasker;
|
||||
private final GuardrailService guardrailService;
|
||||
private final McpRequestContextResolver contextResolver;
|
||||
private final AuditLogService auditLogService;
|
||||
@@ -59,7 +74,6 @@ public class ExecuteService {
|
||||
public ExecuteService(ToolPlanner planner,
|
||||
KillSwitchService killSwitchService,
|
||||
ObjectMapper objectMapper,
|
||||
SensitiveDataMasker dataMasker,
|
||||
GuardrailService guardrailService,
|
||||
McpRequestContextResolver contextResolver,
|
||||
AuditLogService auditLogService,
|
||||
@@ -75,7 +89,6 @@ public class ExecuteService {
|
||||
this.planner = planner;
|
||||
this.killSwitchService = killSwitchService;
|
||||
this.objectMapper = objectMapper;
|
||||
this.dataMasker = dataMasker;
|
||||
this.guardrailService = guardrailService;
|
||||
this.contextResolver = contextResolver;
|
||||
this.auditLogService = auditLogService;
|
||||
@@ -145,22 +158,53 @@ public class ExecuteService {
|
||||
long elapsedMillis = elapsedMillis(startedAt);
|
||||
|
||||
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, "");
|
||||
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
|
||||
|
||||
return result;
|
||||
return finalResult;
|
||||
} catch (ToolExecutionException error) {
|
||||
long elapsedMillis = elapsedMillis(startedAt);
|
||||
auditLogService.toolFinished(context, toolName, 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) {
|
||||
long elapsedMillis = elapsedMillis(startedAt);
|
||||
auditLogService.toolFinished(context, toolName, 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,34 +257,33 @@ public class ExecuteService {
|
||||
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
|
||||
targetUrl = metadata.getPodUrl();
|
||||
}
|
||||
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
|
||||
|
||||
ObjectNode pageArguments = paginationValidator.normalize(arguments);
|
||||
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(metadata, arguments);
|
||||
LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
|
||||
|
||||
while (true) {
|
||||
Map<String, Object> pagePayload = new java.util.HashMap<>(payload);
|
||||
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));
|
||||
}
|
||||
Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
|
||||
|
||||
try {
|
||||
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload))));
|
||||
log.info(" [ExecuteService] 요청 페이로드: {}", objectMapper.writeValueAsString(pagePayload));
|
||||
} catch (Exception ignore) {}
|
||||
|
||||
JsonNode data = null;
|
||||
try {
|
||||
data = toolInvoker.invoke(pagePayload, executeApiUrl);
|
||||
data = toolInvoker.invoke(metadata.getName(), pagePayload, targetUrl, 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) {
|
||||
if (executeApiUrl.contains("http://tool-")) {
|
||||
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
|
||||
if (targetUrl.contains("http://tool-")) {
|
||||
String fallbackUrl = targetUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
|
||||
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
|
||||
try {
|
||||
data = toolInvoker.invoke(pagePayload, fallbackUrl);
|
||||
data = toolInvoker.invoke(metadata.getName(), pagePayload, fallbackUrl, headers);
|
||||
} catch (Exception ex) {
|
||||
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
|
||||
}
|
||||
@@ -337,4 +380,4 @@ public class ExecuteService {
|
||||
public void shutdown() {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.service;
|
||||
package io.shinhanlife.dap.mcg.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -6,16 +6,16 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.service
|
||||
* @package io.shinhanlife.dap.mcg.service
|
||||
* @className KillSwitchService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,9 +1,9 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.service;
|
||||
package io.shinhanlife.dap.mcg.service;
|
||||
|
||||
import io.shinhanlife.dap.common.mcp.security.SecurityProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties;
|
||||
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -12,16 +12,16 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.service
|
||||
* @package io.shinhanlife.dap.mcg.service
|
||||
* @className ToolPlanner
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,15 +1,36 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||
package io.shinhanlife.dap.mcg.sync;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.sync
|
||||
* @className CustomWebMvcSseServerTransportProvider
|
||||
* @description AX HUB MCP Gateway SSE 전송 제공자 - SSE 기반의 MCP 서버 트랜스포트를 구현하는 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </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;
|
||||
@@ -28,6 +49,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
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) {
|
||||
@@ -50,6 +72,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
sessions.clear();
|
||||
customTransports.clear();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,30 +83,30 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
.toList());
|
||||
}
|
||||
|
||||
public org.springframework.web.servlet.mvc.method.annotation.SseEmitter handleSse() {
|
||||
public SseEmitter handleSse() {
|
||||
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);
|
||||
|
||||
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));
|
||||
|
||||
|
||||
// emitter.onCompletion(() -> sessions.remove(sessionId));
|
||||
// emitter.onTimeout(() -> sessions.remove(sessionId));
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
|
||||
emitter.send(SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}).start();
|
||||
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@@ -94,22 +117,29 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
|
||||
org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter = new org.springframework.web.servlet.mvc.method.annotation.SseEmitter(-1L);
|
||||
|
||||
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
|
||||
McpServerSession session = sessionFactory.create(sessionTransport);
|
||||
sessions.put(sessionId, session);
|
||||
boolean isNew = !sessions.containsKey(sessionId);
|
||||
|
||||
emitter.onCompletion(() -> sessions.remove(sessionId));
|
||||
emitter.onTimeout(() -> sessions.remove(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 {
|
||||
// 커스텀 클라이언트는 endpoint 이벤트를 무시할 수 있지만, 표준 호환성을 위해 전송
|
||||
Thread.sleep(100);
|
||||
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
|
||||
|
||||
// Body로 들어온 initialize 등 즉시 처리
|
||||
// Body로 들어온 메시지 즉시 비동기 처리
|
||||
if (body != null && !body.trim().isEmpty()) {
|
||||
handleMessage(sessionId, body);
|
||||
handleMessage(sessionId, body, emitter);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(e);
|
||||
@@ -120,38 +150,52 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
}
|
||||
|
||||
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 org.springframework.http.ResponseEntity.badRequest().body("Missing or invalid sessionId");
|
||||
return ResponseEntity.badRequest().body("Unexpected request body");
|
||||
}
|
||||
|
||||
|
||||
McpServerSession session = sessions.get(sessionId);
|
||||
try {
|
||||
java.util.Map<String, Object> map = objectMapper.readValue(body, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, Object>>() {});
|
||||
io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage message;
|
||||
|
||||
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, io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest.class);
|
||||
message = objectMapper.convertValue(map, JSONRPCRequest.class);
|
||||
} else {
|
||||
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.class);
|
||||
message = objectMapper.convertValue(map, JSONRPCResponse.class);
|
||||
}
|
||||
} else {
|
||||
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification.class);
|
||||
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");
|
||||
return org.springframework.http.ResponseEntity.ok().build();
|
||||
|
||||
if (emitter != null && !map.containsKey("id")) {
|
||||
// emitter.complete();
|
||||
log.info("Completed emitter for notification (disabled for keep-alive)");
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to handle message", e);
|
||||
return org.springframework.http.ResponseEntity.status(500).body(e.getMessage());
|
||||
return ResponseEntity.status(500).body(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSession(String sessionId) {
|
||||
return sessionId != null && sessions.containsKey(sessionId);
|
||||
}
|
||||
|
||||
private class CustomMcpSessionTransport implements McpServerTransport {
|
||||
private final org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter;
|
||||
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) {
|
||||
@@ -159,6 +203,10 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
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(() -> {
|
||||
@@ -166,18 +214,30 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(message);
|
||||
log.info("Serialized message: " + json);
|
||||
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json));
|
||||
log.info("Message successfully sent to SSE emitter");
|
||||
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);
|
||||
emitter.completeWithError(e);
|
||||
if (this.emitter != null) {
|
||||
this.emitter.completeWithError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> closeGracefully() {
|
||||
return Mono.fromRunnable(emitter::complete);
|
||||
return Mono.fromRunnable(() -> {
|
||||
if (this.emitter != null) {
|
||||
this.emitter.complete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1,4 +1,21 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||
package io.shinhanlife.dap.mcg.sync;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.sync
|
||||
* @className DynamicMcpController
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -9,6 +26,7 @@ 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 {
|
||||
@@ -19,24 +37,6 @@ public class DynamicMcpController {
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
@PostMapping("/mcp")
|
||||
public SseEmitter handleSse1() {
|
||||
System.out.println("11111111111");
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/mcp/initialize")
|
||||
public SseEmitter handleSse2() {
|
||||
System.out.println("2222222222");
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/mcp/sse/initialize")
|
||||
public SseEmitter handleSse3() {
|
||||
System.out.println("33333333333333");
|
||||
return null;
|
||||
}
|
||||
|
||||
@GetMapping("/mcp/sse/{category}")
|
||||
public SseEmitter handleSse(@PathVariable("category") String category) {
|
||||
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
|
||||
@@ -70,18 +70,33 @@ public class DynamicMcpController {
|
||||
return ResponseEntity.badRequest().body("Unknown category: " + category);
|
||||
}
|
||||
|
||||
if (sessionId == null || sessionId.isEmpty()) {
|
||||
// 새 세션 생성 (initialize 요청)
|
||||
String newSessionId = java.util.UUID.randomUUID().toString();
|
||||
SseEmitter emitter = transport.handleCustomSse(newSessionId, body);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("Mcp-Session-Id", newSessionId)
|
||||
.body(emitter);
|
||||
} else {
|
||||
// 기존 세션 메시지 전송 (tools/call 등)
|
||||
transport.handleMessage(sessionId, body);
|
||||
return ResponseEntity.accepted().build();
|
||||
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);
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.DeleteMapping("/mcp/sse/{category}")
|
||||
public ResponseEntity<Void> handleDeleteSse(@PathVariable("category") String category) {
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@GetMapping("/.well-known/oauth-protected-resource")
|
||||
public ResponseEntity<java.util.Map<String, Object>> handleOauthProtectedResource() {
|
||||
return ResponseEntity.ok().body(java.util.Map.of());
|
||||
}
|
||||
|
||||
@GetMapping("/.well-known/oauth-protected-resource/**")
|
||||
public ResponseEntity<java.util.Map<String, Object>> handleOauthProtectedResourceWildcard() {
|
||||
return ResponseEntity.ok().body(java.util.Map.of());
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,25 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||
package io.shinhanlife.dap.mcg.sync;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.sync
|
||||
* @className DynamicMcpServerManager
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -36,7 +53,8 @@ public class DynamicMcpServerManager {
|
||||
getOrCreateServer("payment");
|
||||
getOrCreateServer("sms");
|
||||
getOrCreateServer("email");
|
||||
getOrCreateServer("other");
|
||||
getOrCreateServer("oth");
|
||||
getOrCreateServer("sample");
|
||||
}
|
||||
|
||||
private McpSyncServer getOrCreateServer(String categoryKey) {
|
||||
@@ -50,8 +68,8 @@ public class DynamicMcpServerManager {
|
||||
CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper);
|
||||
|
||||
McpSyncServer newServer = McpServer.sync(transport)
|
||||
.serverInfo("AXHUB-Gateway-" + key, "1.0.0")
|
||||
.capabilities(io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.builder().tools(true).build())
|
||||
.serverInfo("DAP-Gateway-" + key, "1.0.0")
|
||||
.capabilities(ServerCapabilities.builder().tools(true).build())
|
||||
.build();
|
||||
|
||||
categoryTransports.put(key, transport);
|
||||
@@ -1,10 +1,25 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||
package io.shinhanlife.dap.mcg.sync;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.sync
|
||||
* @className RegistryMcpToolSpecificationFactory
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.service.ExecuteService;
|
||||
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult;
|
||||
import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -37,6 +52,12 @@ public class RegistryMcpToolSpecificationFactory {
|
||||
.name(entry.getName())
|
||||
.description(description(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();
|
||||
|
||||
return McpServerFeatures.SyncToolSpecification.builder()
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.sync;
|
||||
package io.shinhanlife.dap.mcg.sync;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService;
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -19,16 +19,16 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.biz.mcp.gateway.sync
|
||||
* @package io.shinhanlife.dap.mcg.sync
|
||||
* @className RegistryMcpToolSynchronizer
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@@ -1,7 +1,24 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
|
||||
package io.shinhanlife.dap.mcg.tool.large;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.AgentResponseBudgetProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.tool.large
|
||||
* @className AgentResponseBudgetService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
import io.shinhanlife.dap.mcg.config.AgentResponseBudgetProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -12,12 +29,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
public class AgentResponseBudgetService {
|
||||
private final AgentResponseBudgetProperties properties;
|
||||
private final ObjectMapper json;
|
||||
private final SensitiveDataMasker masker;
|
||||
|
||||
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json, SensitiveDataMasker masker) {
|
||||
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json) {
|
||||
this.properties = properties;
|
||||
this.json = json;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
public ObjectNode apply(ObjectNode response) {
|
||||
@@ -91,7 +105,7 @@ public class AgentResponseBudgetService {
|
||||
}
|
||||
|
||||
private JsonNode budgetItem(JsonNode item, BudgetStats stats) {
|
||||
JsonNode masked = masker.mask(item);
|
||||
JsonNode masked = item;
|
||||
if (!masked.isObject()) {
|
||||
return truncateByBytes(masked, stats);
|
||||
}
|
||||
@@ -99,9 +113,9 @@ public class AgentResponseBudgetService {
|
||||
int copied = 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()) {
|
||||
java.util.Map.Entry<String, JsonNode> entry = fields.next();
|
||||
Map.Entry<String, JsonNode> entry = fields.next();
|
||||
originalFields++;
|
||||
if (copied >= properties.maxFieldsPerItem()) {
|
||||
continue;
|
||||
@@ -135,9 +149,9 @@ public class AgentResponseBudgetService {
|
||||
ObjectNode source = (ObjectNode) item;
|
||||
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()) {
|
||||
java.util.Map.Entry<String, JsonNode> entry = fields.next();
|
||||
Map.Entry<String, JsonNode> entry = fields.next();
|
||||
JsonNode value = entry.getValue();
|
||||
if (value.isTextual()) {
|
||||
limited.put(entry.getKey(), truncateText(value.asText()));
|
||||
@@ -164,7 +178,7 @@ public class AgentResponseBudgetService {
|
||||
fallback.put("maxItemBytes", properties.maxItemBytes());
|
||||
if (item.isObject()) {
|
||||
ArrayNode fieldNames = json.createArrayNode();
|
||||
java.util.Iterator<String> fieldNamesIter = item.fieldNames();
|
||||
Iterator<String> fieldNamesIter = item.fieldNames();
|
||||
while (fieldNamesIter.hasNext()) {
|
||||
fieldNames.add(fieldNamesIter.next());
|
||||
}
|
||||
@@ -188,9 +202,9 @@ public class AgentResponseBudgetService {
|
||||
private ObjectNode copyWithout(ObjectNode source, String... excludedFields) {
|
||||
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()) {
|
||||
java.util.Map.Entry<String, JsonNode> entry = fields.next();
|
||||
Map.Entry<String, JsonNode> entry = fields.next();
|
||||
if (!excluded(entry.getKey(), excludedFields)) {
|
||||
copy.set(entry.getKey(), entry.getValue());
|
||||
}
|
||||
@@ -1,9 +1,25 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
|
||||
package io.shinhanlife.dap.mcg.tool.large;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.tool.large
|
||||
* @className LargeToolResponseService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import java.util.Iterator;
|
||||
|
||||
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.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -17,16 +33,13 @@ import java.time.Instant;
|
||||
public class LargeToolResponseService {
|
||||
private final McpGatewayProperties properties;
|
||||
private final ObjectMapper json;
|
||||
private final SensitiveDataMasker masker;
|
||||
private final AgentResponseBudgetService agentBudget;
|
||||
|
||||
public LargeToolResponseService(McpGatewayProperties properties,
|
||||
ObjectMapper json,
|
||||
SensitiveDataMasker masker,
|
||||
AgentResponseBudgetService agentBudget) {
|
||||
this.properties = properties;
|
||||
this.json = json;
|
||||
this.masker = masker;
|
||||
this.agentBudget = agentBudget;
|
||||
}
|
||||
|
||||
@@ -140,8 +153,7 @@ public class LargeToolResponseService {
|
||||
}
|
||||
Page page = pageFrom(data);
|
||||
if (!page.paginated() && pageCount == 0 && count(page.items()) <= pageSize()) {
|
||||
JsonNode masked = masker.mask(data);
|
||||
normalData = masked;
|
||||
normalData = data;
|
||||
pageCount = 1;
|
||||
returnedCount = count(page.items());
|
||||
totalCount = returnedCount;
|
||||
@@ -254,19 +266,18 @@ public class LargeToolResponseService {
|
||||
}
|
||||
|
||||
private JsonNode previewItem(JsonNode item) {
|
||||
JsonNode masked = masker.mask(item);
|
||||
long itemBytes = jsonBytes(masked);
|
||||
long itemBytes = jsonBytes(item);
|
||||
if (itemBytes <= properties.largeResponseMaxItemBytes()) {
|
||||
return masked;
|
||||
return item;
|
||||
}
|
||||
truncated = true;
|
||||
ObjectNode preview = json.createObjectNode();
|
||||
preview.put("truncated", true);
|
||||
preview.put("originalBytes", itemBytes);
|
||||
preview.put("maxItemBytes", properties.largeResponseMaxItemBytes());
|
||||
if (masked.isObject()) {
|
||||
if (item.isObject()) {
|
||||
ArrayNode fieldNames = json.createArrayNode();
|
||||
java.util.Iterator<String> fieldNamesIter = masked.fieldNames();
|
||||
Iterator<String> fieldNamesIter = item.fieldNames();
|
||||
while (fieldNamesIter.hasNext()) {
|
||||
fieldNames.add(fieldNamesIter.next());
|
||||
}
|
||||
@@ -309,7 +320,7 @@ public class LargeToolResponseService {
|
||||
|
||||
private int count(JsonNode node) {
|
||||
int count = 0;
|
||||
java.util.Iterator<JsonNode> iter = node.elements();
|
||||
Iterator<JsonNode> iter = node.elements();
|
||||
while(iter.hasNext()) {
|
||||
iter.next();
|
||||
count++;
|
||||
@@ -1,8 +1,24 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.tool.large;
|
||||
package io.shinhanlife.dap.mcg.tool.large;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.FailureType;
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.tool.large
|
||||
* @className PaginationRequestValidator
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import io.shinhanlife.dap.lib.dto.ToolMetadata;
|
||||
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 com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
@@ -25,12 +41,18 @@ public class PaginationRequestValidator {
|
||||
/**
|
||||
* pageSize/cursor를 검증한 뒤 Tool 서버에 넘길 안전한 arguments 복사본을 만듭니다.
|
||||
*/
|
||||
public ObjectNode normalize(ObjectNode arguments) {
|
||||
public ObjectNode normalize(ToolMetadata metadata, ObjectNode arguments) {
|
||||
try {
|
||||
ObjectNode normalized = arguments == null
|
||||
? json.createObjectNode()
|
||||
: (ObjectNode) json.readTree(json.writeValueAsString(arguments));
|
||||
normalizePageSize(normalized);
|
||||
|
||||
if (metadata != null && metadata.allowedArguments().contains("pageSize")) {
|
||||
normalizePageSize(normalized);
|
||||
} else if (normalized.has("pageSize")) {
|
||||
normalizePageSize(normalized);
|
||||
}
|
||||
|
||||
validateCursor(normalized);
|
||||
return normalized;
|
||||
} catch (ToolExecutionException error) {
|
||||
@@ -1,5 +1,20 @@
|
||||
package io.shinhanlife.dap.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 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.List;
|
||||
@@ -1,6 +1,20 @@
|
||||
package io.shinhanlife.dap.biz.mcp.gateway.tool.result;
|
||||
package io.shinhanlife.dap.mcg.tool.result;
|
||||
|
||||
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.tool.result
|
||||
* @className ToolExecutionResultFormatter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -19,11 +33,8 @@ public class ToolExecutionResultFormatter {
|
||||
private static final int TEXT_PREVIEW_LIMIT = 2_000;
|
||||
|
||||
private final ObjectMapper json;
|
||||
private final SensitiveDataMasker masker;
|
||||
|
||||
public ToolExecutionResultFormatter(ObjectMapper json, SensitiveDataMasker masker) {
|
||||
public ToolExecutionResultFormatter(ObjectMapper json) {
|
||||
this.json = json;
|
||||
this.masker = masker;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +67,7 @@ public class ToolExecutionResultFormatter {
|
||||
}
|
||||
|
||||
public ToolExecutionResult fromJson(String toolName, JsonNode parsed, long sizeBytes) {
|
||||
JsonNode masked = masker.mask(parsed);
|
||||
JsonNode masked = parsed;
|
||||
if (masked.isObject()) {
|
||||
ObjectNode object = (ObjectNode) masked;
|
||||
if (object.path("isError").asBoolean(false) || object.has("error") || object.has("failureType")) {
|
||||
@@ -0,0 +1,81 @@
|
||||
package io.shinhanlife.dap.mcg.transport;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
import io.modelcontextprotocol.client.McpClient;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import io.shinhanlife.dap.mcg.resilience.FailureType;
|
||||
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** MCP SDK client used for Gateway-to-Tool-Pod calls over Streamable HTTP. */
|
||||
@Slf4j
|
||||
@Component
|
||||
public class HttpToolInvoker implements ToolInvoker {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public HttpToolInvoker(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers) {
|
||||
String endpoint = podUrl.replaceAll("/+$", "") + "/mcp";
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder();
|
||||
if (headers != null) {
|
||||
headers.forEach(requestBuilder::header);
|
||||
}
|
||||
|
||||
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(endpoint)
|
||||
.requestBuilder(requestBuilder)
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
try (McpSyncClient client = McpClient.sync(transport)
|
||||
.clientInfo(new McpSchema.Implementation("dap-gateway", "1.0.0"))
|
||||
.requestTimeout(Duration.ofSeconds(30))
|
||||
.build()) {
|
||||
client.initialize();
|
||||
McpSchema.CallToolResult result = client.callTool(McpSchema.CallToolRequest.builder()
|
||||
.name(toolName)
|
||||
.arguments(arguments)
|
||||
.build());
|
||||
if (Boolean.TRUE.equals(result.isError())) {
|
||||
throw new ToolExecutionException(FailureType.BUSINESS_ERROR, "Tool Pod MCP error: " + textContent(result));
|
||||
}
|
||||
return result.structuredContent() != null
|
||||
? objectMapper.valueToTree(result.structuredContent())
|
||||
: textContentAsJson(result);
|
||||
} catch (ToolExecutionException error) {
|
||||
throw error;
|
||||
} catch (Exception error) {
|
||||
throw new ToolExecutionException(FailureType.SERVER_ERROR,
|
||||
"Tool Pod MCP call failed: " + error.getMessage(), error);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode textContentAsJson(McpSchema.CallToolResult result) {
|
||||
String text = textContent(result);
|
||||
try {
|
||||
return objectMapper.readTree(text);
|
||||
} catch (Exception ignored) {
|
||||
return TextNode.valueOf(text);
|
||||
}
|
||||
}
|
||||
|
||||
private String textContent(McpSchema.CallToolResult result) {
|
||||
return result.content().stream()
|
||||
.filter(McpSchema.TextContent.class::isInstance)
|
||||
.map(McpSchema.TextContent.class::cast)
|
||||
.map(McpSchema.TextContent::text)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.shinhanlife.dap.mcg.transport;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.mcg.transport
|
||||
* @className ToolInvoker
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 0986406
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 0986406 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tool 서버 호출 transport의 최소 공통 인터페이스입니다.
|
||||
*/
|
||||
public interface ToolInvoker {
|
||||
JsonNode invoke(String toolName, Map<String, Object> arguments, String podUrl, Map<String, String> headers);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.converter;
|
||||
|
||||
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
|
||||
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
|
||||
import io.shinhanlife.dap.sample.presentation.io.AppliSystNtfyPatiRequest;
|
||||
import io.shinhanlife.dap.sample.presentation.io.StrnTermRequest;
|
||||
import io.shinhanlife.dap.sample.presentation.io.StrnTermResponse;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.converter
|
||||
* @className SampleConverter
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public abstract class SampleConverter {
|
||||
|
||||
public abstract StrnTermListInDTO convertRequestToDto(StrnTermRequest req);
|
||||
|
||||
public abstract StrnTermResponse convertDtoToResponse(StrnTermListOutDTO outDto);
|
||||
|
||||
public abstract AppliSystNtfyPatiModel convertDtoToModel(AppliSystNtfyRgiInDTO inDto);
|
||||
|
||||
public abstract AppliSystNtfyRgiInDTO convertAppliRequestToDto(AppliSystNtfyPatiRequest request);
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.domain.model;
|
||||
|
||||
import io.shinhanlife.glow.db.dto.AuditInfo;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.domain.model
|
||||
* @className AppliSystNtfyPatiModel
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
public class AppliSystNtfyPatiModel extends AuditInfo {
|
||||
private String appliSystNtfyPatiId;
|
||||
private String appliSystNtfyKdCd;
|
||||
private String appliDutjCd;
|
||||
private String appliDutjPrjcCd;
|
||||
private String ntfyMsgCt;
|
||||
private String ntfyOccDt;
|
||||
private String ntfyCfmDt;
|
||||
private String ntfyTrgtPrafNo;
|
||||
private String shmsPmlYn;
|
||||
private String ntfyCfmYn;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.domain.repository;
|
||||
|
||||
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
|
||||
import io.shinhanlife.glow.GlowIndexPaging;
|
||||
import io.shinhanlife.glow.GlowMybatisMapper;
|
||||
import io.shinhanlife.glow.PageInfo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.domain.repository
|
||||
* @className GlowSampleRepository
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@GlowMybatisMapper
|
||||
public interface GlowSampleRepository {
|
||||
|
||||
@GlowIndexPaging
|
||||
List<StrnTermListOutDTO.StrnTerm> selectStrnTerm(StrnTermListInDTO inDto, PageInfo pageInfo);
|
||||
|
||||
int insertAppliSystNtfyPati(AppliSystNtfyPatiModel appliSystNtfyPatiModel);
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.domain.service;
|
||||
|
||||
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
|
||||
import io.shinhanlife.glow.PageInfo;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.domain.service
|
||||
* @className GlowSampleService
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public interface GlowSampleService {
|
||||
StrnTermListOutDTO getStrnTerms(StrnTermListInDTO inDto, PageInfo pageInfo);
|
||||
int insertAppliSystNtfyPati(AppliSystNtfyRgiInDTO inDto);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.domain.service.impl;
|
||||
|
||||
import io.shinhanlife.dap.sample.converter.SampleConverter;
|
||||
import io.shinhanlife.dap.sample.domain.model.AppliSystNtfyPatiModel;
|
||||
import io.shinhanlife.dap.sample.domain.repository.GlowSampleRepository;
|
||||
import io.shinhanlife.dap.sample.domain.service.GlowSampleService;
|
||||
import io.shinhanlife.dap.sample.dto.AppliSystNtfyRgiInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListInDTO;
|
||||
import io.shinhanlife.dap.sample.dto.StrnTermListOutDTO;
|
||||
import io.shinhanlife.glow.GlowLogger;
|
||||
import io.shinhanlife.glow.GlowLogTarget;
|
||||
import io.shinhanlife.glow.PageInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.domain.service.impl
|
||||
* @className GlowSampleServiceImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GlowSampleServiceImpl implements GlowSampleService {
|
||||
|
||||
@GlowLogTarget(GlowLogTarget.Target.FILE)
|
||||
private final GlowLogger log;
|
||||
private final GlowSampleRepository glowSampleRepository;
|
||||
private final SampleConverter sampleConverter;
|
||||
|
||||
@Override
|
||||
public StrnTermListOutDTO getStrnTerms(StrnTermListInDTO inDto, PageInfo pageInfo) {
|
||||
List<StrnTermListOutDTO.StrnTerm> out = glowSampleRepository.selectStrnTerm(inDto, pageInfo);
|
||||
return StrnTermListOutDTO.builder()
|
||||
.strnTerms(out)
|
||||
.pageInfo(pageInfo)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int insertAppliSystNtfyPati(AppliSystNtfyRgiInDTO inDto) {
|
||||
AppliSystNtfyPatiModel appliSystNtfyPatiModel = sampleConverter.convertDtoToModel(inDto);
|
||||
return glowSampleRepository.insertAppliSystNtfyPati(appliSystNtfyPatiModel);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.dto
|
||||
* @className AppliSystNtfyRgiInDTO
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AppliSystNtfyRgiInDTO {
|
||||
private String appliSystNtfyPatiId;
|
||||
private String appliSystNtfyKdCd;
|
||||
private String appliDutjCd;
|
||||
private String appliDutjPrjcCd;
|
||||
private String ntfyMsgCt;
|
||||
private String ntfyOccDt;
|
||||
private String ntfyCfmDt;
|
||||
private String ntfyTrgtPrafNo;
|
||||
private String shmsPmlYn;
|
||||
private String ntfyCfmYn;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.dto;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.dto
|
||||
* @className StrnTermListInDTO
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StrnTermListInDTO {
|
||||
private String strnTermHanNm;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package io.shinhanlife.dap.sample.dto;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
|
||||
import io.shinhanlife.glow.PageInfo;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dap.sample.dto
|
||||
* @className StrnTermListOutDTO
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author 김형식
|
||||
* @create 2026.09.01
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.09.01 김형식 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StrnTermListOutDTO {
|
||||
private List<StrnTerm> strnTerms;
|
||||
private PageInfo pageInfo;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@Setter
|
||||
public static class StrnTerm{
|
||||
private String strnTermHanNm;
|
||||
private String strnTermEngcAbrNm;
|
||||
private String strnTermEngNm;
|
||||
private String strnTermScrnTermNm;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user