329 Commits

Author SHA1 Message Date
jade
2723d12568 Refactor: rename ToolHint to GrowToolHint and update annotations 2026-08-13 16:20:00 +09:00
jade
df03037d3f update: apply recent changes from local workspace
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-13 09:52:30 +09:00
윤희준
fc235f05b9 ci: configure SizeAndTimeBasedRollingPolicy in logback to prevent disk space issues
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m54s
2026-08-13 09:48:07 +09:00
jade
97d380afe0 feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-12 23:42:14 +09:00
jade
bd4d2a7624 revert: restore register=false check in HeartbeatSender
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 3s
2026-08-12 23:34:42 +09:00
jade
b39c66000a docs: define multi-tool scaffold design 2026-08-12 22:53:57 +09:00
jade
11d78a0119 fix: ignore register=false to expose all tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-12 22:18:00 +09:00
jade
1d4e8b8ebd fix: add missing V17 YAML definition for cmm_customer_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-12 18:24:10 +09:00
jade
a661e07a9f fix: resolve SSE stream completion issue, fix mapstruct typo, and remove duplicate GlowLogger
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-12 18:20:48 +09:00
Boram
4ecff183c6 어노테이션수정
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-12 18:06:25 +09:00
Boram
999b4ebae0 Merge remote-tracking branch 'origin/main'
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-12 17:48:15 +09:00
Boram
69e89e0d52 고객 통합 안내이력 조회 sample 생성 2026-08-12 17:48:00 +09:00
jade
6aa62a5499 fix: preserve Korean text in scaffold output
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m28s
2026-08-12 17:29:10 +09:00
jade
017812cd29 feat: apply tool schema v17 metadata
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 22s
2026-08-12 15:46:57 +09:00
jade
97a56efddf docs: define tool schema v17 migration 2026-08-12 13:12:19 +09:00
binzz
39c59be5a2 Merge remote-tracking branch 'origin/main' into feature/09863413_psb
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m59s
2026-08-12 10:43:26 +09:00
binzz
ccabd1187b feat: add Excel document generator 2026-08-12 10:39:09 +09:00
윤희준
3e553441ba ci: trigger build after static docker cli fix
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m3s
2026-08-12 10:34:58 +09:00
jade
b60b933a08 refactor: remove claim schema sample tool
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m56s
2026-08-12 00:10:04 +09:00
jade
8cbdd2ecad feat: add insurance claim HTTP tool scaffold
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 23:59:33 +09:00
jade
6662641903 refactor: remove sample HTTP tool integration
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 23:41:57 +09:00
jade
0e27937687 feat: add scaffold HTTP mock support
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 23:12:30 +09:00
jade
45e4332adb feat: add AI scaffold drafts
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 22:54:02 +09:00
jade
11f94cf721 feat: add scaffold field editor
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 22:18:45 +09:00
jade
65040bdc8a refactor: remove unused eims senders
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 22:10:58 +09:00
jade
fde039066c refactor: remove legacy eims tool path
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 22:00:22 +09:00
jade
c95b946449 필요 없는 소스 푸쉬
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 18:10:01 +09:00
jade
9c420dc5ab feat: standardize tool names and http clients
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 17:28:11 +09:00
윤희준
95a8ae3a1b ci: add --pull always to docker compose up to automatically update external images
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 16:26:41 +09:00
Boram
9021154481 Merge remote-tracking branch 'origin/main'
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 16:23:03 +09:00
윤희준
e250b1149c ci: update gitea runner registration token
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 16:16:33 +09:00
Boram
494fbe9c45 인터페이스 설계서 자동생성DTO 2026-08-11 16:14:02 +09:00
윤희준
68608569ce ci: exclude bin and act_runner_data from rsync deletion
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-11 15:05:19 +09:00
윤희준
d3ceec615f ci: update docker post-deploy cleanup to prune build cache and unused tagged images
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m35s
2026-08-11 14:57:18 +09:00
jade
8312eee1d1 feat: scaffold Glow HTTP tool integration
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-11 13:22:48 +09:00
jade
041f939c33 fix: honor tool registration flag
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-11 10:28:46 +09:00
jade
4275f76104 feat: enforce underscore MCP tool names
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-11 10:18:36 +09:00
jade
40303ee9b8 feat: add Glow HTTP tool integration sample
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-10 17:24:28 +09:00
jade
4b08021fc6 fix: add AUTO-TESTER to tenant-domains in application.yml to allow DeepBuilder tool execution
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m34s
2026-08-10 16:05:44 +09:00
jade
f2e1c8d388 fix: correct scaffold package generation
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m38s
2026-08-10 16:01:14 +09:00
jade
2712993286 fix: skip domain authorization check for internal system tenant
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m36s
2026-08-10 15:58:23 +09:00
jade
568b6e1d63 feat: add smp to pre-initialized category list in DynamicMcpServerManager
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m32s
2026-08-10 15:47:34 +09:00
jade
15bb39bdcb feat: force all tools to be registered to Redis regardless of register=false
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-10 15:47:10 +09:00
jade
4d929da2ce fix: set register=true for Onnba3011UseCase to expose it to MCP Gateway
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m36s
2026-08-10 15:44:58 +09:00
jade
cf0a25bac3 fix: uncomment emitter.complete() for notifications to prevent client hang
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m37s
2026-08-10 15:40:48 +09:00
jade
25f55e4455 fix: uncomment emitter.complete() to close stream for custom protocol
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m41s
2026-08-10 15:36:55 +09:00
jade
5fa086531d fix: change serverInfo prefix from DAP-Gateway to dap-was in DynamicMcpServerManager
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
2026-08-10 15:29:23 +09:00
jade
2f643a626c docs: clarify internal java image usage in dockerfile
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m34s
2026-08-10 11:05:04 +09:00
jade
bc5f224e22 docs: add internal network migration checklist
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m38s
2026-08-10 10:48:17 +09:00
jade
906e7e206d fix: fall back to tool pods when redis is unavailable
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m14s
2026-08-10 10:29:53 +09:00
jade
dd36247bf7 feat: standardize glow profile configuration
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m39s
2026-08-10 09:45:13 +09:00
jade
7819b11ce3 feat: opt in to output schema validation
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m41s
2026-08-10 09:24:05 +09:00
jade
6809435cc2 feat: cache tool methods on pod startup
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
2026-08-10 09:06:56 +09:00
jade
813c846f05 feat: scaffold tools from declared fields
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m39s
2026-08-09 17:50:06 +09:00
jade
c0567ec1c2 fix: generate schema based tool request DTOs
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m36s
2026-08-09 17:13:54 +09:00
jade
ae00489a39 feat: align pod scaffolding with oth runtime
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m37s
2026-08-09 16:54:24 +09:00
jade
03203a9912 feat: unify MCP tool execution and chat callbacks
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m2s
2026-08-09 16:11:46 +09:00
jade
7c9077dbd1 chore: align scaffold gradle with tool standard
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m38s
2026-08-07 16:56:07 +09:00
jade
782dee0d19 chore: downgrade to spring boot 3.5
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m43s
2026-08-07 13:07:08 +09:00
jade
d1f202aa9a Update sms tool name and categoryKey to sms
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m46s
2026-08-06 01:20:39 +09:00
jade
2d9e42b533 Fix invalid MCP tool names for Onnba3011 and SmsTool
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-06 01:19:15 +09:00
jade
7287a96a01 Fix scaffold errors and apply ToolHint annotations
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 33s
2026-08-06 01:14:53 +09:00
jade
6394ac651b fix: example 입력
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-05 14:34:23 +09:00
jade
ac60d046eb fix: example 입력
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m24s
2026-08-05 14:19:37 +09:00
jade
3a86a8e0c7 fix: align tool module handling with dap-was naming
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-05 13:44:35 +09:00
jade
ae8cc38bcf fix: 로컬 JS 라이브러리(Tailwind, Chart.js) 누락 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m58s
2026-08-05 11:07:10 +09:00
jade
bf369e2a83 feat: MCP Tool Tester UI/UX 고도화 (Stress 테스트, 실시간 로그 콘솔, Auto-Ping 등 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-05 10:59:40 +09:00
jade
4b34df774d refactor: restructure packages, delete ZtUsac, fix gateway context
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m15s
2026-08-05 10:09:36 +09:00
jade
65222b5b1a feat: index.html 메인 화면에 Tester, Console 카드 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m49s
2026-08-05 00:06:02 +09:00
jade
b9f98e89ce fix: index.html 헤더 링크 추가 및 tester.html CSV 한글 깨짐 방지(BOM 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m49s
2026-08-05 00:01:59 +09:00
jade
96e308aefb fix: ZtUsacRepository (MyBatis Mapper) 의존성 주입 에러 수정 (MybatisConfig에 @MapperScan 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 23:56:23 +09:00
jade
96a26bc93d fix: ToolManifestService 등의 Bean 로드 에러 방지를 위해 scanBasePackages 범위를 io.shinhanlife.dap.lib 전체로 확장
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m50s
2026-08-04 23:53:58 +09:00
jade
a2586fd6d3 fix: ToolPodMcpToolSynchronizer가 dap-gateway에서도 스캔되어 발생하는 Bean 에러 수정 (usecase 패키지로 이동)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m48s
2026-08-04 23:51:07 +09:00
jade
cdfb6ef901 fix: ToolRegistryHeartbeatSender Bean을 인식하지 못하는 런타임 에러 수정 (scanBasePackages 추가)
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-08-04 23:46:30 +09:00
jade
3729e2d6ee fix: McpRouterController, ChatController 불필요한 FQCN 선언 제거 및 import 통합
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 23:43:44 +09:00
jade
946ec6d70f chore: DapToolOthApplication, logback 등 코드 내 남아있던 이전 명칭(찌꺼기) 일괄 치환 및 정리
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m7s
2026-08-04 23:36:50 +09:00
jade
136ad2fa71 refactor: dap-tool-* 모듈을 dap-was-* 이름으로 전면 개편 및 컨테이너명/파이프라인 연동
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m57s
2026-08-04 23:33:06 +09:00
jade
3e69436bfe chore: 잘못 생성되어 있던 중복 ToolMetadata 클래스(mcg.dto 패키지) 삭제
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-04 23:10:02 +09:00
jade
f827760513 refactor: 공용 클래스(MCP, Manifest, UseCase 등) lib 패키지로 이관 및 통합
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
2026-08-04 23:06:01 +09:00
jade
1a70495686 feat(ui): add tool-test-console and tester dashboards
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
- Add Auto-Tester Dashboard (tester.html) in gateway for batch testing tools

- Add standalone Tool Test Console (tool-test-console.html) in core

- Fix Tailwind CSS Preflight conflicts in console UI

- Update console payload schema resolution to support both gateway and pod modes

- Strip JSON-RPC metadata from tool payload output in console

- Add unified navigation headers across all static HTML pages
2026-08-04 18:24:52 +09:00
jade
39c1f4ee12 UI: chat.html 상단 GNB 디자인을 scaffold.html과 동일하게 동기화
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-04 16:45:57 +09:00
jade
e858859021 feat: standardize tool names by pod domain action
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-04 16:39:03 +09:00
jade
a58af1606e feat: publish tool manifest endpoint
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m50s
2026-08-04 15:58:59 +09:00
jade
08ccc043da feat: organize tool schemas by category
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m58s
2026-08-04 14:42:08 +09:00
jade
4393071212 chore: log validated tool response
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-04 13:38:43 +09:00
jade
1bac8d9d87 feat: enrich claim search output schema
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-08-04 13:34:02 +09:00
jade
31e6fd605d feat: support MCP output schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-08-04 11:24:59 +09:00
jade
15262d5a0f test: keep input schemas in tool modules
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m56s
2026-08-04 11:03:35 +09:00
jade
2957272cc3 feat: support explicit MCP input schemas
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-04 10:50:25 +09:00
jade
52f585fa45 Refactor: Remove SensitiveDataMasker and fix PaginationRequestValidator
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m55s
2026-08-03 17:41:02 +09:00
jade
bc86f36825 feat(scaffold): 템플릿 코드에 @McpValidation 예시 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m42s
2026-08-03 16:37:11 +09:00
jade
55dcffa2c9 feat(schema): 고급 JSON Schema 제약조건(format, default, examples 등) 지원 및 McpFunction 수정 반영
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-08-03 16:35:46 +09:00
jade
be0314afe8 feat(schema): @McpAnyOf 어노테이션 및 JSON Schema 파싱 로직 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m56s
2026-08-03 16:31:43 +09:00
jade
39eab0429e Revert "feat(security): ToolSecurityContext 및 KMS 복호화 공통 모듈 추가"
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
This reverts commit cf958622bd.
2026-08-03 15:35:20 +09:00
jade
cf958622bd feat(security): ToolSecurityContext 및 KMS 복호화 공통 모듈 추가
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-03 13:45:16 +09:00
jade
d858f2ab90 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m53s
2026-07-31 14:59:17 +09:00
jade
2bc896a130 application 업데이트 2026-07-31 14:59:00 +09:00
b22cd779cf Merge pull request 'fix: paser로 인해 인공지능의 띄어쓰기가 이상해지는 문제 해결' (#5) from feature/markdown into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m20s
Reviewed-on: #5
2026-07-31 14:48:01 +09:00
juheelee
b3908f771f fix: paser로 인해 인공지능의 띄어쓰기가 이상해지는 문제 해결 2026-07-31 14:45:31 +09:00
jade
58234b1c59 refactor: align Glow transaction field metadata
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m20s
2026-07-31 14:44:21 +09:00
jade
541d6d1938 config: centralize Glow MCI settings
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m15s
2026-07-31 14:41:35 +09:00
628925dce9 Merge pull request 'feature/markdown' (#4) from feature/markdown into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m58s
Reviewed-on: #4
2026-07-30 18:01:02 +09:00
juheelee
9d16b8ef3a feat: markdown 추가 2026-07-30 17:59:27 +09:00
juheelee
39f68ccf46 feat: markdown 추가 2026-07-30 17:59:06 +09:00
jade
1bf09705d1 build: validate duplicate MCP tool names before packaging
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-30 17:41:06 +09:00
jade
60055dd9ed feat: propagate MCP request headers to tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m28s
2026-07-30 17:10:15 +09:00
jade
b5ff930556 docs: refresh project README
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-30 10:16:25 +09:00
Gitea CI
2c4c447a8f feat: OpenRouter 실존 무료 모델 교체 및 Gemini API 라우팅 전환
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m9s
- chat.html: 드롭다운을 검증된 무료 모델로 교체
  * Ling 3.0 Flash (inclusionai/ling-3.0-flash:free) 기본값으로 설정
  * GPT-OSS 20B, Gemma 4 31B, NVIDIA Nemotron Nano, Cohere North Mini 추가
  * 삭제된 모델(qwen-2-7b, llama-3-8b, openchat-7b 등) 제거
- ChatController.java: Gemini 직접 API(비정상 키) 제거
  * gemini-flash-latest 선택 시 OpenRouter Google Gemma 4 31B 로 라우팅
  * 불필요한 70여 라인 직접 HTTP 호출 코드 제거
- E2E 검증 완료: Ling 3.0 Flash 정상 응답 확인
2026-07-30 07:28:56 +09:00
hjgram
68e1bf95fd merge: remote updates into main with tool-core schema updates
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m17s
2026-07-30 06:26:41 +09:00
hjgram
204dd04d5f feat: OCI 배포 듀얼 LLM 스위칭 및 신규 툴 DTO 의존성 주입 병합 2026-07-30 06:19:06 +09:00
jade
5fe8590695 feat: use MCP SDK for tool pod calls
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-07-29 23:03:11 +09:00
0a2464d426 Merge pull request 'feat: 메타 테이블 조회' (#3) from feature/meta-table-check into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m21s
Reviewed-on: #3
2026-07-29 17:30:32 +09:00
juheelee
a667431de1 feat: 메타 테이블 조회 2026-07-29 17:27:27 +09:00
mjh
aca0e009bc Merge feature/20260729-mjh: add SOL request detail tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-07-29 17:17:37 +09:00
df537fa77c Merge pull request 'DEV-SRTEST' (#2) from DEV-SRTEST into main
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m51s
Reviewed-on: #2
2026-07-29 17:04:40 +09:00
jade
8b4d4da0b9 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool into DEV-SRTEST 2026-07-29 17:02:21 +09:00
jade
c551b15378 테스트 푸쉬 2026-07-29 17:01:05 +09:00
juheelee
b448e68f45 Merge branch 'main' of https://git.devjun.net/kimhyungsik/ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m49s
2026-07-29 16:50:56 +09:00
juheelee
b68fbb9726 feat: 메타 통합 코드 조회 2026-07-29 16:48:28 +09:00
c6a2f82455 push TEST
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m16s
2026-07-29 16:08:45 +09:00
juheelee
2f5dad05df feat: 메타 통합 코드 조회 2026-07-29 15:34:01 +09:00
ea1aea81bb feat: add SOL request detail tool 2026-07-29 14:26:38 +09:00
jade
b0bf4a9c68 fix: use rsync to clean stale files during host volume sync
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m1s
2026-07-29 13:25:10 +09:00
jade
752aae4dde fix: run gradle natively in runner to avoid isolated mount issue
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m50s
2026-07-29 13:22:03 +09:00
jade
5cb37cfda6 fix: mount correct app path for gradle build
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 4s
2026-07-29 13:16:31 +09:00
jade
2a4b3e68e9 fix: allow gradle build libs to be copied into docker container
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 2m3s
2026-07-29 13:03:11 +09:00
jade
ca1082f1c2 fix: rollback register to false as per user request
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
2026-07-29 12:58:22 +09:00
jade
7b1d3131e6 fix: set register true for tools to fix catalog display
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 33s
2026-07-29 12:55:06 +09:00
jade
139ddbc6ca fix: optimize dockerfile to use external gradle build
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s
2026-07-29 12:54:02 +09:00
jade
870ebb723f fix: hide markdown emphasis markers in chat responses
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m22s
2026-07-29 11:35:22 +09:00
jade
a08a7d25df refactor: move team member tool to smp category
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-29 11:30:52 +09:00
jade
60acc5df81 refactor: rename solgit tool to sol
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m23s
2026-07-29 11:23:49 +09:00
jade
f5be5b2514 refactor: remove unused team member legacy types
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m32s
2026-07-29 11:14:44 +09:00
jade
aae4898d18 feat: invoke Solgit MCI before returning dummy response
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m26s
2026-07-29 11:02:10 +09:00
jade
06b724459e feat: add solgitReqList and get_mcp_tool_team_members tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m31s
2026-07-29 10:26:32 +09:00
jade
4133b63595 불필요 한 파일 삭제
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m19s
2026-07-28 14:39:50 +09:00
jade
1e18d2bc47 feat: support nested MCP input schema validation
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-28 14:39:31 +09:00
jade
0c89a09ccf docs: Update README.md with Web UI Scaffolder enhancements
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 8m5s
2026-07-27 15:41:32 +09:00
jade
3ce9201cd5 fix: Add throws Exception to MciClient template 2026-07-27 15:37:27 +09:00
jade
be81c095c9 fix: Correct text block alignment and formatting arguments 2026-07-27 15:36:40 +09:00
jade
d4554ce34f fix: Remove excessive indentation in string formatting for UseCaseImpl text block 2026-07-27 15:30:58 +09:00
jade
b6d4ab1bec fix: Remove duplicated package path in MCI DTOs and Converter 2026-07-27 15:26:38 +09:00
jade
2c8497ebee fix: Correct ToolScaffolder template issues 2026-07-27 15:10:15 +09:00
jade
8a93d5d6dc feat: Support 4-letter MCI system code in Scaffolder and refactor Onnba classes 2026-07-27 15:03:13 +09:00
jade
c0de1f4edb fix: Include local fallback tools in generateToolsMarkdown API
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m15s
2026-07-27 14:11:25 +09:00
jade
1ee9f9e802 style: Add chat.html link to all top navigation bars 2026-07-27 14:07:33 +09:00
jade
25f0d2bab5 feat: move Onnba MCI request conversion to use case
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m1s
2026-07-27 14:01:04 +09:00
jade
9b9159a605 feat: Add UI/UX improvements to catalog.html including search, config json copy, and guide modal
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 34s
2026-07-27 14:00:27 +09:00
jade
c8dd43221e ci: Fix Node.js installation by dropping sudo/curl and supporting alpine
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 6m41s
2026-07-27 13:09:01 +09:00
jade
7b8c38489e ci: Add Node.js installation step to Gitea Actions deploy workflow
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-27 13:07:29 +09:00
jade
63187429b4 feat: validate MCP tool input constraints
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-27 13:01:40 +09:00
jade
d4953d60ec test: fix gateway test package references
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 18s
2026-07-27 12:51:28 +09:00
jade
72891dc2db 불필요한 소스 삭제
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 12:38:31 +09:00
jade
82c27bd468 test: prevent duplicate MCP tool names
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 12:37:35 +09:00
jade
ff5ca4fc7a Fix: Add missing formatted arguments for MCI and EAI ToolScaffolder templates 2026-07-27 11:43:37 +09:00
jade
ba39a23b09 Fix: Add missing @Service annotations to MCP Tool use case implementations
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 10:45:09 +09:00
jade
579979b5fe Refactor: Move MCP tool annotations to interface level 2026-07-27 10:21:24 +09:00
jade
db46f30e72 refactor: DTO 레코드를 별도 파일(dto 패키지)로 분리
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 10:06:43 +09:00
jade
2c64d86a56 refactor: UseCase 인터페이스 내 DTO 분리 및 강결합 제거
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 10:04:56 +09:00
jade
ded5ef9e8f 신한 라이프 경로에 맞게 수정
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-27 08:57:03 +09:00
jade
a6ca2b82a7 scaffold 수정
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-26 16:00:45 +09:00
jade
94cdaaa8c9 불필요 없는 소스 삭제
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-26 15:46:48 +09:00
jade
bea517c4c1 fix: resolve bean injection error and gateway routing configs
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-26 15:34:43 +09:00
jade
ad950071b2 refactor: fix annotation package - remove duplicate annotation.annotation directory
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 02:00:47 +09:00
jade
e6407b07a2 refactor: apply Glow standard to dap-tool-core (usecase, javadoc, etc)
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:56:41 +09:00
jade
e58c004d9d refactor: rename biz/other to biz/oth
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:43:47 +09:00
jade
92fe2399be refactor: rename dap-tool-other to dap-tool-oth
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:36:12 +09:00
jade
7ba18f7809 refactor: rename domain categories (common->cmm, sample->smp) and restrict category key to 3 chars
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:29:48 +09:00
jade
4dcc5a2978 refactor: restructure MCP tools without Controller layer, using UseCase pattern per Glow standards
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 01:13:25 +09:00
jade
994f5b4348 fix: remove docker compose down from deploy pipeline to prevent killing the CI runner
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 00:13:57 +09:00
jade
0a50b20960 feat: add chat.html link to index.html dashboard
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 00:13:00 +09:00
jade
97ba0c3267 fix: add --remove-orphans to docker compose down to prevent port conflicts with legacy Blue/Green containers
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-25 00:09:36 +09:00
jade
388fa33c67 fix: revert Nginx and Blue/Green deployment to restore stable Apache connection
Some checks are pending
Deploy to OCIWP / deploy (push) Waiting to run
2026-07-25 00:05:54 +09:00
jade
4015207b86 fix: map Nginx 8281 to host 8080 to restore Apache proxy connection
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 7m53s
2026-07-24 23:52:55 +09:00
jade
4dcc85eff0 fix: override gateway fallback URLs via env vars to support blue/green
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 23:42:30 +09:00
jade
72f04aef62 fix: remove curl command that fails in act_runner
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 11m35s
2026-07-24 23:26:48 +09:00
jade
b5565a4da0 fix: repair blue-green deployment architecture
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 54s
2026-07-24 23:24:55 +09:00
jade
c38d59887c fix: remove 8080 from deploy script
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 10m59s
2026-07-24 23:04:38 +09:00
jade
f3639043fc chore: fetch gateway logs in deploy action 2026-07-24 17:52:15 +09:00
jade
e278602d32 fix: map gateway to 8281 to match Apache 2026-07-24 17:51:38 +09:00
jade
f6f86e5afb chore: test curl to gateway inside deploy action 2026-07-24 17:51:03 +09:00
jade
a89843518b fix: restore gateway 8081 port mapping 2026-07-24 17:48:34 +09:00
jade
d7b80a644c fix: bypass nginx and map gateway directly to 8281 2026-07-24 17:47:41 +09:00
jade
05a242e5de chore: test nginx static HTML response 2026-07-24 17:46:46 +09:00
jade
a409652bdf fix: Nginx port 8281 mapping restored 2026-07-24 17:46:09 +09:00
jade
fa268edf7d fix: Nginx SSL support for Apache proxy 2026-07-24 17:44:23 +09:00
jade
c69f11e1c9 fix: add http2 support to Nginx to match Apache h2c proxying
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m41s
2026-07-24 17:42:25 +09:00
jade
56483d804a chore: test nginx proxy to google
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 17:41:33 +09:00
jade
1c353ce3a7 chore: test nginx plain response
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 17:39:36 +09:00
jade
35f361a976 fix: restore Nginx port 8281 mapping - Apache VirtualHost proxies to 8281
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m41s
2026-07-24 16:08:59 +09:00
jade
9066c621a0 fix: resolve Nginx 503 error on rollback and apply package refactoring
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 11m44s
2026-07-24 15:00:01 +09:00
jade
4ed534b1d6 fix: restore Nginx 8080 port mapping for Apache and refactor MciCfpaClient
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m42s
2026-07-24 14:27:00 +09:00
jade
0892597c31 fix: start containers sequentially and fix nginx startup config
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m20s
2026-07-24 13:29:43 +09:00
jade
6416d3f844 fix: revert Nginx port mapping to 8281 to match external Apache proxy
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 13:21:10 +09:00
jade
dbe479a64b fix: build sequentially to avoid OOM on dev server
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m8s
2026-07-24 13:04:42 +09:00
jade
53f5047c8e fix: map Nginx 8281 to host 8080 to restore Apache proxy connection
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 12m3s
2026-07-24 11:44:01 +09:00
jade
f56e503a29 fix: make Blue-Green deployment safe by checking Nginx reload status and removing redundant host Gradle build to prevent OOM server crashes
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 10m6s
2026-07-24 10:55:32 +09:00
jade
aae95591a0 fix: add Nginx SSE proxy configs to prevent buffering of AI streams, and fix Gateway internal routing to use Nginx
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 10:45:27 +09:00
jade
227edf9744 fix: remove stale src directories in host volume before copy to resolve compilation conflict
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 12m14s
2026-07-24 10:28:24 +09:00
jade
12ac05c805 fix: add clean to gradle build in Gitea deploy pipeline to clear persistent host volume cache
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 38s
2026-07-24 10:26:09 +09:00
jade
74ea89bf1c refactor: apply naming convention for MCI interface IO objects (CLCNNB00001_I, CUST_INQ_001_I) and separate layers
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 24s
2026-07-24 10:24:00 +09:00
jade
d562e35ad8 fix: resolve stale class conflict in Docker build by adding clean and .dockerignore
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 23s
2026-07-24 10:20:00 +09:00
jade
fc8c3da666 refactor: apply MSA naming standard for DTOs and Scaffold template (Req->Request, Res->Response)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 30s
2026-07-24 10:07:50 +09:00
jade
438c190447 refactor: rename packages dapms to mcg and dapmt to mcc
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 42s
2026-07-24 10:00:45 +09:00
jade
51f37cfcbc chore: add restart always policy to nginx
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 14m33s
2026-07-24 09:45:23 +09:00
jade
44c2fcebba fix: invalid docker filter syntax in deploy.yml
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m9s
2026-07-24 09:15:02 +09:00
jade
2758f6da8e fix: Nginx port conflict with legacy containers
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14m45s
2026-07-24 08:55:30 +09:00
jade
8b11c1aae8 feat: 적용 무중단 배포 시스템 (Blue-Green Deployment)
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 51s
2026-07-24 08:51:01 +09:00
jade
60a7226199 chore: remove unnecessary Node.js installation from deploy.yml
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m32s
2026-07-24 00:29:02 +09:00
jade
b3bdaa2b28 fix: revert ports and add zombie container cleanup to CI
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-24 00:22:20 +09:00
jade
260f59b143 fix: change dozzle port to 18288 to resolve conflict
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13m26s
2026-07-24 00:02:54 +09:00
jade
ad2ab985d8 feat: allow Chat AI to use unregistered (register=false) tools
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13m58s
2026-07-23 23:46:04 +09:00
jade
18959b5128 fix: install nodejs for checkout action
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 23:32:44 +09:00
jade
7f85b10938 chore: remove accidentally committed SearchHr generated files
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-23 23:30:53 +09:00
jade
b5dbcc54f0 fix: add checkout step and sync code to host volume in deployment pipeline
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 8s
2026-07-23 23:28:29 +09:00
jade
abf7a435a4 feat/fix: update ToolScaffolder interface, update scaffold.html, fix mci-mock port
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14m20s
2026-07-23 23:11:30 +09:00
jade
f0a295632f ci: add docker system prune before deployment
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 16m15s
2026-07-23 22:52:48 +09:00
jade
cec56e015b fix: change Redis host port mapping to avoid conflicts
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 11m48s
2026-07-23 22:33:09 +09:00
kimhyungsik
1e54370fb8 fix: restore missing closing brace in DynamicMcpController
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15m36s
2026-07-23 13:08:16 +09:00
kimhyungsik
408a3f6b28 fix: add DELETE mapping and .well-known endpoints for Cursor compatibility
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m23s
2026-07-23 13:05:20 +09:00
jade
541150d3f4 Fix: gateway local profile fallback URL을 컨테이너 이름으로 변경
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 13:01:53 +09:00
jade
165a6340ea Refactor: ToolService 인터페이스 분리 및 GlowEaiComponent 로드 에러 수정
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 12:59:34 +09:00
kimhyungsik
5f8228bfce fix: add EnableScheduling to enable dynamic tool synchronization
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17m7s
2026-07-23 11:16:54 +09:00
kimhyungsik
6893bb6b35 fix: comment out emitter.onCompletion/onTimeout session removal to keep sessions persistent
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 14m21s
2026-07-23 10:33:56 +09:00
jade
3c183e901b chore: Remove hardcoded axhub.tool.url from application-dev.yml
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m57s
2026-07-23 10:17:38 +09:00
kimhyungsik
2948465b8a fix: comment out emitter.complete() to allow keep-alive sessions for standard SSE MCP client
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 10:16:15 +09:00
jade
2b4adb45d2 fix: Correct tool-payment internal port mapping to 8085
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-23 10:10:45 +09:00
jade
323d227eee feat: Add AxhubEaiComponent and MCI/EAI mock frameworks
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 3m26s
2026-07-23 09:19:21 +09:00
kimhyungsik
74b50482bf chore: optimize dev fallback routes to use internal docker service names
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 13m24s
2026-07-22 17:29:00 +09:00
kimhyungsik
dfa5a73b20 chore: force docker compose project name to ax_hub_mcp_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 7m48s
2026-07-22 16:44:29 +09:00
kimhyungsik
86302d0947 chore: optimize deploy pipeline to ultra-fast relay
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-22 16:31:13 +09:00
kimhyungsik
ad37714c5b chore_fix_docker_compose_typo
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 13m31s
2026-07-22 16:01:03 +09:00
jade
b57d96f83f refactor: rename project codes mcg to dapms and mcc to dapmt
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s
2026-07-22 15:48:45 +09:00
kimhyungsik
fabbd368a2 chore_resolve_path_and_arg_issues
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 57s
2026-07-22 15:37:33 +09:00
kimhyungsik
f3cdfaccc3 chore_apply_gradle_jdk21_build_and_compose_direct_mount
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 34s
2026-07-22 15:33:57 +09:00
kimhyungsik
e42ca92811 chore_resolve_git_conflict
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 15s
2026-07-22 15:31:13 +09:00
jade
c18efe251e refactor: migrate all properties files to yaml
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:57:44 +09:00
jade
8d4e8fd976 chore: replace author name with employee ID 0986406
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1s
2026-07-22 14:36:15 +09:00
kimhyungsik
edec314a83 test: final verify auto deploy
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:08:45 +09:00
kimhyungsik
da09997452 chore: enable docker run relay deploy
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:05:46 +09:00
kimhyungsik
5c15ae3505 test: verify auto deploy triggering
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:03:22 +09:00
kimhyungsik
3eebf07b0e chore: restore standard runs-on label
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 14:00:36 +09:00
kimhyungsik
a161dcf319 chore: quote runs-on host label
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-22 13:59:46 +09:00
kimhyungsik
ae3352ccb4 chore: fix runs-on syntax and add gradle caching
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 13:57:00 +09:00
kimhyungsik
2deffd3f10 chore: change runs-on to host mode
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled
2026-07-22 13:52:22 +09:00
kimhyungsik
f334b8ecc5 feat: add deploy workflow
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-07-22 13:47:01 +09:00
jade
1db19344bc fix: restore broken Korean comments in application-dev.properties and add UTF-8 encoding rule 2026-07-22 13:36:22 +09:00
jade
8e6682d089 chore: migrate environment endpoints from render to OCI 2026-07-22 13:35:01 +09:00
jade
6968c7dfb7 refactor: use Spring DI for MapStruct and update AGENTS.md rule 2026-07-22 12:37:16 +09:00
jade
416b068fcb 소스 삭제 2026-07-22 11:36:29 +09:00
jade
933ba2ecfa 소스 수정 2026-07-22 11:35:56 +09:00
jade
ce96137948 fix: resolve Format specifier '%s' error in ToolScaffolder and generate SearchHr tool 2026-07-22 11:31:39 +09:00
jade
e983b47af6 feat: add MapStruct mapping samples to ToolScaffolder template 2026-07-22 11:22:36 +09:00
jade
1796370a35 docs: add Shinhan Life logging standard rules to AGENTS.md 2026-07-22 10:40:16 +09:00
jade
680dbe098b feat: apply Shinhan Life logging standards to all modules 2026-07-22 10:22:16 +09:00
jade
a877103d93 fix: add missing java.util.Map import and remove fully qualified names in ToolScaffolder template 2026-07-22 09:54:57 +09:00
jade
6e1ef40380 fix: use Map instead of JSON string for fallback returns in ToolScaffolder and SearchHrService to resolve serialization and IDE warnings 2026-07-22 09:51:27 +09:00
jade
a59249e1db fix: always generate legacy DTOs and converter regardless of routing type 2026-07-22 09:24:07 +09:00
jade
128fc3d022 feat: update ToolScaffolder to generate legacy DTOs in 'legacy' and converters in 'converter' package 2026-07-22 09:22:50 +09:00
jade
899053aef5 소스 경로 변경 2026-07-22 09:21:05 +09:00
jade
0a5bc52e6a 소스 경로 변경 2026-07-22 09:14:14 +09:00
jade
885963c81c 소스 경로 변경 2026-07-21 22:33:35 +09:00
jade
b8a8da3b3c 소스 경로 변경 2026-07-21 22:29:06 +09:00
jade
1a30c3a0cd 필요 없는 소스 삭제 2026-07-21 22:19:09 +09:00
jade
50b6b8b9b0 remove: Delete MciSampleTool and related DTOs 2026-07-21 22:18:10 +09:00
jade
140be4c7e9 fix: Scaffold dropdown missing modules on Render 2026-07-21 22:11:21 +09:00
jade
75b93d47c2 fix: Restore Korean character encoding in application-dev.properties 2026-07-21 21:57:34 +09:00
jade
f880f391d5 fix: Update fallback URLs from dap-tool to axhub-tool 2026-07-21 21:55:40 +09:00
jade
8a53bff74f Revert "refactor: Remove fallback HTTP logic from McpRouterController"
This reverts commit a46943ca23.
2026-07-21 21:53:36 +09:00
jade
a46943ca23 refactor: Remove fallback HTTP logic from McpRouterController 2026-07-21 21:49:32 +09:00
jade
11b8b08890 redis 연동 테스트 2026-07-21 21:30:28 +09:00
jade
2138ab7efa feat: Add MCI Scaffolding support and fix bean collision 2026-07-21 21:22:38 +09:00
jade
7dca243380 fix: add io.shinhanlife.dap.common.integration to component scan paths across all application modules 2026-07-21 17:09:46 +09:00
jade
c90654bd1b refactor: move AxhubMciComponent and GlowMockConfig to core module for common usage 2026-07-21 17:07:02 +09:00
jade
334af3e40e fix: register GlowMciComponent as Spring Bean via config to resolve IntelliJ autowiring error 2026-07-21 17:02:57 +09:00
jade
ea27ed621a fix: resolve IntelliJ autowiring and inspection warnings for AxhubMciComponent 2026-07-21 16:59:52 +09:00
jade
fbe7091975 refactor: rename LicoMciComponent to AxhubMciComponent 2026-07-21 16:57:10 +09:00
jade
7ccca1812f refactor: apply GlowMciComponent architecture as requested by Shinhan Life standard 2026-07-21 16:53:31 +09:00
jade
123fdb30a9 fix: correct Onnba3011ReqDto fields from new API spec and fix OnnbaMciToolService compile error 2026-07-21 16:42:43 +09:00
jade
0ed927f845 feat: update Onnba3011ReqDto fields from API spec 2026-07-21 16:38:54 +09:00
jade
60ac3bc4be refactor: extract Onnba3011ReqDto into separate file 2026-07-21 16:32:40 +09:00
jade
0afa489165 feat: add OnnbaMciToolService converted from legacy MCI code 2026-07-21 15:56:37 +09:00
jade
8cd07a255c fix: add sample category to pre-initialized servers to avoid 400 bad request 2026-07-21 15:13:31 +09:00
jade
cc98d173ad fix: set register=true for sample tools to enable auto-registration on Render 2026-07-21 15:04:25 +09:00
jade
c450f26d53 fix: correct gateway URL in application-dev.properties for all tools 2026-07-21 14:55:06 +09:00
jade
7660db3c38 fix: disable auto namespace prefix in PodScaffolder 2026-07-21 14:24:49 +09:00
jade
beed6e7001 docs: add sample category to scaffolder and readme 2026-07-21 14:22:22 +09:00
jade
8612dd6ea7 feat: rename sample tools 2026-07-21 14:19:08 +09:00
jade
db6ed5b09a feat: add get_exchange_rate and get_daily_quote tools and set categoryKey to sample 2026-07-21 14:17:36 +09:00
jade
db949be3f3 feat: generate and log new request-id for outbound HTTP calls in WeatherTool 2026-07-21 14:12:58 +09:00
jade
9edcdab450 관련 소스 수정 2026-07-21 14:05:39 +09:00
jade
82b1e6e267 feat: rename tool get_weather to weather 2026-07-21 14:04:48 +09:00
jade
e35b378d61 fix: wrap execute tool response to match expected client format 2026-07-21 14:00:51 +09:00
jade
f8d6ed8da3 feat: remove namespace prefix from all remaining tool modules 2026-07-21 13:52:37 +09:00
jade
437d061bef feat: remove namespace prefix from dap-tool-other 2026-07-21 13:50:54 +09:00
jade
9cce77a142 fix: add missing register=false to register_vacation tool 2026-07-21 13:45:03 +09:00
jade
115aafa220 fix: swap trace-id and request-id header mapping 2026-07-21 13:43:13 +09:00
jade
da7ed2e28b fix: remove duplicate register property in TemplateUtilityService 2026-07-21 13:32:40 +09:00
jade
48ac7b9f9f chore: disable redis heartbeat registration for all tools (register=false) 2026-07-21 13:26:02 +09:00
jade
580edd98a6 feat: add trace-id and request-id header logging to tool controller 2026-07-21 13:18:38 +09:00
jade
b9966bbbda feat: rename weather tool from get_current_weather to get_weather 2026-07-21 13:10:52 +09:00
jade
438e30f987 refactor: change internal gateway-to-tool routing from JSON-RPC to REST API 2026-07-21 12:50:28 +09:00
jade
fe9e00b5fb feat: add missing globalId to SlCommonHeaderDto 2026-07-21 10:48:13 +09:00
jade
b0f12b578d feat: add SL and OL standard header DTOs from spec 2026-07-21 10:44:17 +09:00
jade
39513a1470 feat: apply full spec to ShinhanCommonHeaderDto (42 fields) 2026-07-21 10:36:34 +09:00
jade
821b249d50 fix: resolve MCI sample tool execution errors and improve parameter definitions in UI 2026-07-21 10:29:30 +09:00
jade
e02a4e24e8 fix: Add dap-tool-core to dap-gateway Dockerfile 2026-07-21 09:06:34 +09:00
jade
6b8ca5581f fix: Remove COPY dap-common from Dockerfiles 2026-07-21 09:04:07 +09:00
jade
d6869f8191 refactor: Merge dap-common into dap-tool-core 2026-07-21 09:02:48 +09:00
jade
515ab409ec fix: Update BusinessToolController to use generateSchema 2026-07-20 17:15:49 +09:00
jade
b55f863de9 fix: Complete JsonSchemaGenerator with required properties for MCP compliance 2026-07-20 17:07:39 +09:00
jade
80ff392b5a feat: Add SampleMciToolService for MCI integration sample 2026-07-20 16:44:12 +09:00
jade
6dd1fe6cda refactor: apply Glow Framework package naming conventions (mcg, mcc)
- io.shinhanlife.dap.biz.mcp.gateway -> io.shinhanlife.dap.mcg
- io.shinhanlife.dap.biz.mcp.tool -> io.shinhanlife.dap.mcc
- io.shinhanlife.dap.biz.mcp.adapter -> io.shinhanlife.dap.common.adapter
- io.shinhanlife.dap.biz.mcp.sample -> io.shinhanlife.dap.mcc.sample
2026-07-20 11:39:36 +09:00
jade
2162587a46 fix: 딥 빌더 테스트 도구 실행 시 system 권한 도메인 허용 설정 추가 2026-07-20 10:28:11 +09:00
jade
736593c516 fix: Deep Agent Builder 무한 대기 현상 해결을 위해 JSON-RPC Notification 처리 후 SSE 강제 종료 2026-07-20 10:15:07 +09:00
jade
bd85ca9f88 fix: Deep Agent Builder JSON 파싱 에러 방지를 위해 커스텀 SSE 엔드포인트 이벤트 제거 2026-07-20 09:53:44 +09:00
jade
d6801b7ba0 fix: Deep Agent Builder 호환성을 위한 mcp-http Warm Pool 세션 버그 및 CORS 헤더 수정 2026-07-20 09:36:15 +09:00
jade
2e6b28e1c1 log 경로 수정 2026-07-20 09:01:44 +09:00
jade
0ebac4e375 McpFunction에 MCP 표준 힌트 4개 추가 2026-07-17 18:08:12 +09:00
jade
8359371122 필요 없는 부분 삭제 2026-07-17 17:38:13 +09:00
jade
7eef3a65c5 refactor: Fix all inline FQ names with proper imports and add Javadoc to 67 files 2026-07-17 15:20:26 +09:00
jade
467289bf9f skill 추가 2026-07-17 01:06:51 +09:00
jade
c05bfb0ab3 skill 추가 2026-07-17 01:05:27 +09:00
jade
ac15d38468 skill 추가 2026-07-17 01:01:24 +09:00
jade
87f367b2c1 git 변경 테스트 2026-07-17 00:50:06 +09:00
jade
5a5e67629d chore: Add explicit import for CustomWebMvcSseServerTransportProvider 2026-07-16 21:37:57 +09:00
jade
e1591761d2 refactor: Replace inline fully qualified package names with proper imports 2026-07-16 21:36:51 +09:00
jade
c982696631 git 변경 테스트 2026-07-16 21:25:40 +09:00
jade
2c0c0a3790 fix: Rename AXHUB-Gateway to DAP-Gateway in DynamicMcpServerManager 2026-07-16 21:11:11 +09:00
550 changed files with 21191 additions and 8679 deletions

View File

@@ -5,9 +5,10 @@
## 개발 가이드라인 ## 개발 가이드라인
* 패키지명은 `controller` 대신 `presentation`을 사용합니다. * 패키지명은 `controller` 대신 `presentation`을 사용합니다.
* MapStruct 사용 시, 테스트 환경 에러를 방지하기 위해 생성자(`new ...Impl()`) 대신 `Mappers.getMapper(인터페이스명.class)` 방식으로 인스턴스를 가져옵니다. * MapStruct 사용 시, Spring DI를 활용하여 의존성 주입(`private final Converter converter;`)을 받는 방식을 권장합니다. (단위 테스트 시에는 `@MockBean` 또는 직접 구현체를 주입하여 테스트)
## 명심해야 할 규칙 추가란 ## 명심해야 할 규칙 추가란
* `application.yml` 등 설정 파일 수정 시 한글이 깨지지 않도록 항상 UTF-8 인코딩을 유지하고, 깨진 문자열(`?\uFFFD` 등)이 발생하지 않도록 각별히 주의한다.
* 이모지는 무조건 넣지 않는다 * 이모지는 무조건 넣지 않는다
* import 할것 무조건 한다 * import 할것 무조건 한다
* Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다. * Git commit과 push는 사용자의 명시적인 허락(지시) 없이는 절대 수행하지 않는다.
@@ -16,13 +17,13 @@
* @package io.shinhanlife.axhub.biz.mcp.tool.sms * @package io.shinhanlife.axhub.biz.mcp.tool.sms
* @className AxHubToolSmsApplication * @className AxHubToolSmsApplication
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */
@@ -30,3 +31,9 @@
* 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외) * 현재 개발 대상은 **대내MCI / EAI (JSON)** 연동으로 한정한다. (대외MCI FixedLength 연동은 범위에서 제외)
* 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행) * 통신 노선, 데이터 변환 규격, 시스템별 연계 방식 등은 MCI/EIMS 상에서 관리되므로 코드 레벨에서 식별하거나 분기 처리하지 않는다. (단순 통합 JSON 요청만 수행)
* 처리계 UI ↔ 처리계 AP 구간: `HTTPS` / `SSV`를 사용하며, FW에서 x-api를 통해 SSV↔DTO 변환을 수행한다. * 처리계 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}" />` 를 선언하여 사용한다.

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
#!/bin/bash
# Spring Boot 로그 파일 경로 (기본 설정: ./logs/spring-boot-logger.log)
LOG_FILE="./logs/spring-boot-logger.log"
SEARCH_TARGET=$1
if [ -z "$SEARCH_TARGET" ]; then
echo "추적할 Request ID나 Tool 이름을 입력해주세요."
exit 1
fi
if [ ! -f "$LOG_FILE" ]; then
echo "오류: 로그 파일($LOG_FILE)을 찾을 수 없습니다. 경로를 확인해주세요."
exit 1
fi
echo "[$SEARCH_TARGET] 라우팅 추적 시작..."
# Spring Boot 로그에서 대상 검색하여 주요 필드 출력
grep -i "$SEARCH_TARGET" "$LOG_FILE" | awk '{print $1, $2, $3, $5, $NF}'

View File

@@ -0,0 +1,16 @@
---
name: mcp-schema-autogen
description: Java 21 Record 클래스나 Spring Controller 코드를 분석하여 MCP 표준 JSON Schema(Tool 명세)를 자동 생성합니다.
scope: workspace
---
# 시스템 지침 (System Prompt)
너는 MCP(Model Context Protocol) Tool 스키마 전문 엔지니어다.
사용자가 Java 코드를 제공하면 다음 규칙에 따라 JSON Schema를 작성해라.
1. **타입 매핑:** Java 21 `Record`의 필드 타입을 완벽하게 JSON Schema 타입(string, integer, boolean, object, array 등)으로 매핑할 것.
2. **필수값 추출:** `@NotNull`, `@NotBlank` 또는 Java 21 Record의 Non-null 필드는 반드시 `required` 배열에 포함시킬 것.
3. **설명 추가:** JavaDoc 주석이 있거나 필드 위에 주석이 있다면 스키마의 `description` 필드로 적극 반영할 것.
# 출력 형식
결과물은 반드시 순수한 JSON 형태로만 출력하고, 불필요한 부연 설명은 생략한다.

View File

@@ -0,0 +1,249 @@
# MCP Server Best Practices
## Quick Reference
### Server Naming
- **Python**: `{service}_mcp` (e.g., `slack_mcp`)
- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`)
### Tool Naming
- Use snake_case with service prefix
- Format: `{service}_{action}_{resource}`
- Example: `slack_send_message`, `github_create_issue`
### Response Formats
- Support both JSON and Markdown formats
- JSON for programmatic processing
- Markdown for human readability
### Pagination
- Always respect `limit` parameter
- Return `has_more`, `next_offset`, `total_count`
- Default to 20-50 items
### Transport
- **Streamable HTTP**: For remote servers, multi-client scenarios
- **stdio**: For local integrations, command-line tools
- Avoid SSE (deprecated in favor of streamable HTTP)
---
## Server Naming Conventions
Follow these standardized naming patterns:
**Python**: Use format `{service}_mcp` (lowercase with underscores)
- Examples: `slack_mcp`, `github_mcp`, `jira_mcp`
**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens)
- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server`
The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers.
---
## Tool Naming and Design
### Tool Naming
1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info`
2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers
- Use `slack_send_message` instead of just `send_message`
- Use `github_create_issue` instead of just `create_issue`
3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.)
4. **Be specific**: Avoid generic names that could conflict with other servers
### Tool Design
- Tool descriptions must narrowly and unambiguously describe functionality
- Descriptions must precisely match actual functionality
- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
- Keep tool operations focused and atomic
---
## Response Formats
All tools that return data should support multiple formats:
### JSON Format (`response_format="json"`)
- Machine-readable structured data
- Include all available fields and metadata
- Consistent field names and types
- Use for programmatic processing
### Markdown Format (`response_format="markdown"`, typically default)
- Human-readable formatted text
- Use headers, lists, and formatting for clarity
- Convert timestamps to human-readable format
- Show display names with IDs in parentheses
- Omit verbose metadata
---
## Pagination
For tools that list resources:
- **Always respect the `limit` parameter**
- **Implement pagination**: Use `offset` or cursor-based pagination
- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count`
- **Never load all results into memory**: Especially important for large datasets
- **Default to reasonable limits**: 20-50 items is typical
Example pagination response:
```json
{
"total": 150,
"count": 20,
"offset": 0,
"items": [...],
"has_more": true,
"next_offset": 20
}
```
---
## Transport Options
### Streamable HTTP
**Best for**: Remote servers, web services, multi-client scenarios
**Characteristics**:
- Bidirectional communication over HTTP
- Supports multiple simultaneous clients
- Can be deployed as a web service
- Enables server-to-client notifications
**Use when**:
- Serving multiple clients simultaneously
- Deploying as a cloud service
- Integration with web applications
### stdio
**Best for**: Local integrations, command-line tools
**Characteristics**:
- Standard input/output stream communication
- Simple setup, no network configuration needed
- Runs as a subprocess of the client
**Use when**:
- Building tools for local development environments
- Integrating with desktop applications
- Single-user, single-session scenarios
**Note**: stdio servers should NOT log to stdout (use stderr for logging)
### Transport Selection
| Criterion | stdio | Streamable HTTP |
|-----------|-------|-----------------|
| **Deployment** | Local | Remote |
| **Clients** | Single | Multiple |
| **Complexity** | Low | Medium |
| **Real-time** | No | Yes |
---
## Security Best Practices
### Authentication and Authorization
**OAuth 2.1**:
- Use secure OAuth 2.1 with certificates from recognized authorities
- Validate access tokens before processing requests
- Only accept tokens specifically intended for your server
**API Keys**:
- Store API keys in environment variables, never in code
- Validate keys on server startup
- Provide clear error messages when authentication fails
### Input Validation
- Sanitize file paths to prevent directory traversal
- Validate URLs and external identifiers
- Check parameter sizes and ranges
- Prevent command injection in system calls
- Use schema validation (Pydantic/Zod) for all inputs
### Error Handling
- Don't expose internal errors to clients
- Log security-relevant errors server-side
- Provide helpful but not revealing error messages
- Clean up resources after errors
### DNS Rebinding Protection
For streamable HTTP servers running locally:
- Enable DNS rebinding protection
- Validate the `Origin` header on all incoming connections
- Bind to `127.0.0.1` rather than `0.0.0.0`
---
## Tool Annotations
Provide annotations to help clients understand tool behavior:
| Annotation | Type | Default | Description |
|-----------|------|---------|-------------|
| `readOnlyHint` | boolean | false | Tool does not modify its environment |
| `destructiveHint` | boolean | true | Tool may perform destructive updates |
| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect |
| `openWorldHint` | boolean | true | Tool interacts with external entities |
**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations.
---
## Error Handling
- Use standard JSON-RPC error codes
- Report tool errors within result objects (not protocol-level errors)
- Provide helpful, specific error messages with suggested next steps
- Don't expose internal implementation details
- Clean up resources properly on errors
Example error handling:
```typescript
try {
const result = performOperation();
return { content: [{ type: "text", text: result }] };
} catch (error) {
return {
isError: true,
content: [{
type: "text",
text: `Error: ${error.message}. Try using filter='active_only' to reduce results.`
}]
};
}
```
---
## Testing Requirements
Comprehensive testing should cover:
- **Functional testing**: Verify correct execution with valid/invalid inputs
- **Integration testing**: Test interaction with external systems
- **Security testing**: Validate auth, input sanitization, rate limiting
- **Performance testing**: Check behavior under load, timeouts
- **Error handling**: Ensure proper error reporting and cleanup
---
## Documentation Requirements
- Provide clear documentation of all tools and capabilities
- Include working examples (at least 3 per major feature)
- Document security considerations
- Specify required permissions and access levels
- Document rate limits and performance characteristics

View File

@@ -0,0 +1,602 @@
# MCP Server Evaluation Guide
## Overview
This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided.
---
## Quick Reference
### Evaluation Requirements
- Create 10 human-readable questions
- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE
- Each question requires multiple tool calls (potentially dozens)
- Answers must be single, verifiable values
- Answers must be STABLE (won't change over time)
### Output Format
```xml
<evaluation>
<qa_pair>
<question>Your question here</question>
<answer>Single verifiable answer</answer>
</qa_pair>
</evaluation>
```
---
## Purpose of Evaluations
The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions.
## Evaluation Overview
Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be:
- Realistic
- Clear and concise
- Unambiguous
- Complex, requiring potentially dozens of tool calls or steps
- Answerable with a single, verifiable value that you identify in advance
## Question Guidelines
### Core Requirements
1. **Questions MUST be independent**
- Each question should NOT depend on the answer to any other question
- Should not assume prior write operations from processing another question
2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use**
- Should not instruct or require modifying state to arrive at the correct answer
3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX**
- Must require another LLM to use multiple (potentially dozens of) tools or steps to answer
### Complexity and Depth
4. **Questions must require deep exploration**
- Consider multi-hop questions requiring multiple sub-questions and sequential tool calls
- Each step should benefit from information found in previous questions
5. **Questions may require extensive paging**
- May need paging through multiple pages of results
- May require querying old data (1-2 years out-of-date) to find niche information
- The questions must be DIFFICULT
6. **Questions must require deep understanding**
- Rather than surface-level knowledge
- May pose complex ideas as True/False questions requiring evidence
- May use multiple-choice format where LLM must search different hypotheses
7. **Questions must not be solvable with straightforward keyword search**
- Do not include specific keywords from the target content
- Use synonyms, related concepts, or paraphrases
- Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer
### Tool Testing
8. **Questions should stress-test tool return values**
- May elicit tools returning large JSON objects or lists, overwhelming the LLM
- Should require understanding multiple modalities of data:
- IDs and names
- Timestamps and datetimes (months, days, years, seconds)
- File IDs, names, extensions, and mimetypes
- URLs, GIDs, etc.
- Should probe the tool's ability to return all useful forms of data
9. **Questions should MOSTLY reflect real human use cases**
- The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about
10. **Questions may require dozens of tool calls**
- This challenges LLMs with limited context
- Encourages MCP server tools to reduce information returned
11. **Include ambiguous questions**
- May be ambiguous OR require difficult decisions on which tools to call
- Force the LLM to potentially make mistakes or misinterpret
- Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER
### Stability
12. **Questions must be designed so the answer DOES NOT CHANGE**
- Do not ask questions that rely on "current state" which is dynamic
- For example, do not count:
- Number of reactions to a post
- Number of replies to a thread
- Number of members in a channel
13. **DO NOT let the MCP server RESTRICT the kinds of questions you create**
- Create challenging and complex questions
- Some may not be solvable with the available MCP server tools
- Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN)
- Questions may require dozens of tool calls to complete
## Answer Guidelines
### Verification
1. **Answers must be VERIFIABLE via direct string comparison**
- If the answer can be re-written in many formats, clearly specify the output format in the QUESTION
- Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else."
- Answer should be a single VERIFIABLE value such as:
- User ID, user name, display name, first name, last name
- Channel ID, channel name
- Message ID, string
- URL, title
- Numerical quantity
- Timestamp, datetime
- Boolean (for True/False questions)
- Email address, phone number
- File ID, file name, file extension
- Multiple choice answer
- Answers must not require special formatting or complex, structured output
- Answer will be verified using DIRECT STRING COMPARISON
### Readability
2. **Answers should generally prefer HUMAN-READABLE formats**
- Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d
- Rather than opaque IDs (though IDs are acceptable)
- The VAST MAJORITY of answers should be human-readable
### Stability
3. **Answers must be STABLE/STATIONARY**
- Look at old content (e.g., conversations that have ended, projects that have launched, questions answered)
- Create QUESTIONS based on "closed" concepts that will always return the same answer
- Questions may ask to consider a fixed time window to insulate from non-stationary answers
- Rely on context UNLIKELY to change
- Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later
4. **Answers must be CLEAR and UNAMBIGUOUS**
- Questions must be designed so there is a single, clear answer
- Answer can be derived from using the MCP server tools
### Diversity
5. **Answers must be DIVERSE**
- Answer should be a single VERIFIABLE value in diverse modalities and formats
- User concept: user ID, user name, display name, first name, last name, email address, phone number
- Channel concept: channel ID, channel name, channel topic
- Message concept: message ID, message string, timestamp, month, day, year
6. **Answers must NOT be complex structures**
- Not a list of values
- Not a complex object
- Not a list of IDs or strings
- Not natural language text
- UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON
- And can be realistically reproduced
- It should be unlikely that an LLM would return the same list in any other order or format
## Evaluation Process
### Step 1: Documentation Inspection
Read the documentation of the target API to understand:
- Available endpoints and functionality
- If ambiguity exists, fetch additional information from the web
- Parallelize this step AS MUCH AS POSSIBLE
- Ensure each subagent is ONLY examining documentation from the file system or on the web
### Step 2: Tool Inspection
List the tools available in the MCP server:
- Inspect the MCP server directly
- Understand input/output schemas, docstrings, and descriptions
- WITHOUT calling the tools themselves at this stage
### Step 3: Developing Understanding
Repeat steps 1 & 2 until you have a good understanding:
- Iterate multiple times
- Think about the kinds of tasks you want to create
- Refine your understanding
- At NO stage should you READ the code of the MCP server implementation itself
- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks
### Step 4: Read-Only Content Inspection
After understanding the API and tools, USE the MCP server tools:
- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY
- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions
- Should NOT call any tools that modify state
- Will NOT read the code of the MCP server implementation itself
- Parallelize this step with individual sub-agents pursuing independent explorations
- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations
- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT
- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration
- In all tool call requests, use the `limit` parameter to limit results (<10)
- Use pagination
### Step 5: Task Generation
After inspecting the content, create 10 human-readable questions:
- An LLM should be able to answer these with the MCP server
- Follow all question and answer guidelines above
## Output Format
Each QA pair consists of a question and an answer. The output should be an XML file with this structure:
```xml
<evaluation>
<qa_pair>
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
<answer>Website Redesign</answer>
</qa_pair>
<qa_pair>
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
<answer>sarah_dev</answer>
</qa_pair>
<qa_pair>
<question>Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs?</question>
<answer>7</answer>
</qa_pair>
<qa_pair>
<question>Find the repository with the most stars that was created before 2023. What is the repository name?</question>
<answer>data-pipeline</answer>
</qa_pair>
</evaluation>
```
## Evaluation Examples
### Good Questions
**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)**
```xml
<qa_pair>
<question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question>
<answer>Python</answer>
</qa_pair>
```
This question is good because:
- Requires multiple searches to find archived repositories
- Needs to identify which had the most forks before archival
- Requires examining repository details for the language
- Answer is a simple, verifiable value
- Based on historical (closed) data that won't change
**Example 2: Requires understanding context without keyword matching (Project Management MCP)**
```xml
<qa_pair>
<question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question>
<answer>Product Manager</answer>
</qa_pair>
```
This question is good because:
- Doesn't use specific project name ("initiative focused on improving customer onboarding")
- Requires finding completed projects from specific timeframe
- Needs to identify the project lead and their role
- Requires understanding context from retrospective documents
- Answer is human-readable and stable
- Based on completed work (won't change)
**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)**
```xml
<qa_pair>
<question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question>
<answer>alex_eng</answer>
</qa_pair>
```
This question is good because:
- Requires filtering bugs by date, priority, and status
- Needs to group by assignee and calculate resolution rates
- Requires understanding timestamps to determine 48-hour windows
- Tests pagination (potentially many bugs to process)
- Answer is a single username
- Based on historical data from specific time period
**Example 4: Requires synthesis across multiple data types (CRM MCP)**
```xml
<qa_pair>
<question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question>
<answer>Healthcare</answer>
</qa_pair>
```
This question is good because:
- Requires understanding subscription tier changes
- Needs to identify upgrade events in specific timeframe
- Requires comparing contract values
- Must access account industry information
- Answer is simple and verifiable
- Based on completed historical transactions
### Poor Questions
**Example 1: Answer changes over time**
```xml
<qa_pair>
<question>How many open issues are currently assigned to the engineering team?</question>
<answer>47</answer>
</qa_pair>
```
This question is poor because:
- The answer will change as issues are created, closed, or reassigned
- Not based on stable/stationary data
- Relies on "current state" which is dynamic
**Example 2: Too easy with keyword search**
```xml
<qa_pair>
<question>Find the pull request with title "Add authentication feature" and tell me who created it.</question>
<answer>developer123</answer>
</qa_pair>
```
This question is poor because:
- Can be solved with a straightforward keyword search for exact title
- Doesn't require deep exploration or understanding
- No synthesis or analysis needed
**Example 3: Ambiguous answer format**
```xml
<qa_pair>
<question>List all the repositories that have Python as their primary language.</question>
<answer>repo1, repo2, repo3, data-pipeline, ml-tools</answer>
</qa_pair>
```
This question is poor because:
- Answer is a list that could be returned in any order
- Difficult to verify with direct string comparison
- LLM might format differently (JSON array, comma-separated, newline-separated)
- Better to ask for a specific aggregate (count) or superlative (most stars)
## Verification Process
After creating evaluations:
1. **Examine the XML file** to understand the schema
2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF
3. **Flag any operations** that require WRITE or DESTRUCTIVE operations
4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document
5. **Remove any `<qa_pair>`** that require WRITE or DESTRUCTIVE operations
Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end.
## Tips for Creating Quality Evaluations
1. **Think Hard and Plan Ahead** before generating tasks
2. **Parallelize Where Opportunity Arises** to speed up the process and manage context
3. **Focus on Realistic Use Cases** that humans would actually want to accomplish
4. **Create Challenging Questions** that test the limits of the MCP server's capabilities
5. **Ensure Stability** by using historical data and closed concepts
6. **Verify Answers** by solving the questions yourself using the MCP server tools
7. **Iterate and Refine** based on what you learn during the process
---
# Running Evaluations
After creating your evaluation file, you can use the provided evaluation harness to test your MCP server.
## Setup
1. **Install Dependencies**
```bash
pip install -r scripts/requirements.txt
```
Or install manually:
```bash
pip install anthropic mcp
```
2. **Set API Key**
```bash
export ANTHROPIC_API_KEY=your_api_key_here
```
## Evaluation File Format
Evaluation files use XML format with `<qa_pair>` elements:
```xml
<evaluation>
<qa_pair>
<question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question>
<answer>Website Redesign</answer>
</qa_pair>
<qa_pair>
<question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question>
<answer>sarah_dev</answer>
</qa_pair>
</evaluation>
```
## Running Evaluations
The evaluation script (`scripts/evaluation.py`) supports three transport types:
**Important:**
- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually.
- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL.
### 1. Local STDIO Server
For locally-run MCP servers (script launches the server automatically):
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a my_mcp_server.py \
evaluation.xml
```
With environment variables:
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a my_mcp_server.py \
-e API_KEY=abc123 \
-e DEBUG=true \
evaluation.xml
```
### 2. Server-Sent Events (SSE)
For SSE-based MCP servers (you must start the server first):
```bash
python scripts/evaluation.py \
-t sse \
-u https://example.com/mcp \
-H "Authorization: Bearer token123" \
-H "X-Custom-Header: value" \
evaluation.xml
```
### 3. HTTP (Streamable HTTP)
For HTTP-based MCP servers (you must start the server first):
```bash
python scripts/evaluation.py \
-t http \
-u https://example.com/mcp \
-H "Authorization: Bearer token123" \
evaluation.xml
```
## Command-Line Options
```
usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND]
[-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL]
[-H HEADERS [HEADERS ...]] [-o OUTPUT]
eval_file
positional arguments:
eval_file Path to evaluation XML file
optional arguments:
-h, --help Show help message
-t, --transport Transport type: stdio, sse, or http (default: stdio)
-m, --model Claude model to use (default: claude-3-7-sonnet-20250219)
-o, --output Output file for report (default: print to stdout)
stdio options:
-c, --command Command to run MCP server (e.g., python, node)
-a, --args Arguments for the command (e.g., server.py)
-e, --env Environment variables in KEY=VALUE format
sse/http options:
-u, --url MCP server URL
-H, --header HTTP headers in 'Key: Value' format
```
## Output
The evaluation script generates a detailed report including:
- **Summary Statistics**:
- Accuracy (correct/total)
- Average task duration
- Average tool calls per task
- Total tool calls
- **Per-Task Results**:
- Prompt and expected response
- Actual response from the agent
- Whether the answer was correct (✅/❌)
- Duration and tool call details
- Agent's summary of its approach
- Agent's feedback on the tools
### Save Report to File
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a my_server.py \
-o evaluation_report.md \
evaluation.xml
```
## Complete Example Workflow
Here's a complete example of creating and running an evaluation:
1. **Create your evaluation file** (`my_evaluation.xml`):
```xml
<evaluation>
<qa_pair>
<question>Find the user who created the most issues in January 2024. What is their username?</question>
<answer>alice_developer</answer>
</qa_pair>
<qa_pair>
<question>Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name.</question>
<answer>backend-api</answer>
</qa_pair>
<qa_pair>
<question>Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take?</question>
<answer>127</answer>
</qa_pair>
</evaluation>
```
2. **Install dependencies**:
```bash
pip install -r scripts/requirements.txt
export ANTHROPIC_API_KEY=your_api_key
```
3. **Run evaluation**:
```bash
python scripts/evaluation.py \
-t stdio \
-c python \
-a github_mcp_server.py \
-e GITHUB_TOKEN=ghp_xxx \
-o github_eval_report.md \
my_evaluation.xml
```
4. **Review the report** in `github_eval_report.md` to:
- See which questions passed/failed
- Read the agent's feedback on your tools
- Identify areas for improvement
- Iterate on your MCP server design
## Troubleshooting
### Connection Errors
If you get connection errors:
- **STDIO**: Verify the command and arguments are correct
- **SSE/HTTP**: Check the URL is accessible and headers are correct
- Ensure any required API keys are set in environment variables or headers
### Low Accuracy
If many evaluations fail:
- Review the agent's feedback for each task
- Check if tool descriptions are clear and comprehensive
- Verify input parameters are well-documented
- Consider whether tools return too much or too little data
- Ensure error messages are actionable
### Timeout Issues
If tasks are timing out:
- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`)
- Check if tools are returning too much data
- Verify pagination is working correctly
- Consider simplifying complex questions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.gradle
.idea
/build/
target/
*/target/
bin/
*/bin/
out/
*/out/

View File

@@ -0,0 +1,68 @@
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' --exclude='bin' --exclude='act_runner_data' . /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 --pull always --remove-orphans gateway redis mci-mock dozzle was-sms was-oth
# 5. 배포 후 대롱대롱 매달려 있는 가비지 이미지 및 빌드 캐시 자동 소거 청소!
docker image prune -a -f
docker builder prune -a -f
echo "CI/CD Deploy Success!"

62
.gitignore vendored
View File

@@ -56,11 +56,69 @@ build/
# ===== VS Code ===== # ===== VS Code =====
.vscode/ .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 ===== # ===== OS =====
.DS_Store .DS_Store
Thumbs.db Thumbs.db
ehthumbs.db ehthumbs.db
# ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? ===== # ===== ?섍꼍?ㅼ젙 (誘쇨컧?뺣낫 遺꾨━ ?? =====
# application-local.properties # application-local.yml
# application-secret.properties # application-secret.yml

View File

@@ -1,30 +1,50 @@
# 1. 鍮뚮뱶 ?섍꼍 (JDK 21) # 1. 빌드 환경 (JDK 21)
# -----------------------------------------------------------------------------
# 외부 인터넷이 차단된 내부망에서는 Docker Hub 대신 사내 Container Registry의
# Java 21 이미지를 사용합니다. 인프라 담당자에게 이미지의 전체 경로와 태그를
# 받은 후 아래 FROM 행만 교체합니다.
#
# 예: FROM registry.shinhanlife.co.kr/base/openjdk:21 AS builder
#
# 이 단계는 Gradle 빌드와 Java 컴파일을 수행하므로 반드시 JDK 21 이미지여야 합니다.
# 사내 이미지가 실제로 JDK 21인지 다음 명령으로 확인합니다.
# docker run --rm <사내-JDK-이미지> java -version
FROM eclipse-temurin:21-jdk-alpine AS builder FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app WORKDIR /app
# Gradle ?섑띁?€ ?뚯뒪 蹂듭궗 # Gradle Wrapper와 소스 파일을 복사합니다.
COPY gradlew . COPY gradlew .
COPY gradle gradle COPY gradle gradle
COPY build.gradle settings.gradle ./ COPY build.gradle settings.gradle ./
COPY src src COPY src src
# 沅뚰븳 遺€??諛?鍮뚮뱶 (?뚯뒪???쒖쇅) # Wrapper 실행 권한을 부여하고 테스트를 제외한 빌드를 수행합니다.
RUN chmod +x gradlew RUN chmod +x gradlew
RUN ./gradlew clean build -x test RUN ./gradlew clean build -x test
# 2. ?ㅽ뻾 ?섍꼍 (JRE 21) # 2. 실행 환경 (JRE 21)
# -----------------------------------------------------------------------------
# 실행 단계는 JRE 21 이미지가 가장 가볍지만, 사내에서 JDK 21 이미지만 제공하는
# 경우에는 동일한 JDK 21 이미지를 사용해도 정상 동작합니다.
#
# 예: FROM registry.shinhanlife.co.kr/base/openjre:21
# 예: FROM registry.shinhanlife.co.kr/base/openjdk:21
#
# 사내 제공 이미지의 기반 OS를 확인합니다.
# - Alpine 기반: 아래 apk 명령을 그대로 사용합니다.
# - Ubuntu/Debian 기반: apk 대신 apt-get update && apt-get install -y tzdata를 사용합니다.
FROM eclipse-temurin:21-jre-alpine FROM eclipse-temurin:21-jre-alpine
WORKDIR /app WORKDIR /app
# ?€?꾩〈 ?ㅼ젙 (?쒓뎅 ?쒓컙) # 서울 시간대를 설정합니다.
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul ENV TZ=Asia/Seoul
# 鍮뚮뱶??JAR ?뚯씪 蹂듭궗 # 빌드 단계에서 생성한 애플리케이션 JAR를 복사합니다.
COPY --from=builder /app/build/libs/*.jar app.jar COPY --from=builder /app/build/libs/*.jar app.jar
# 湲곕낯 ?ы듃 ?몄텧 # 애플리케이션 포트를 노출합니다.
EXPOSE 8081 EXPOSE 8081
# 而⑦뀒?대꼫 ?ㅽ뻾 ??JAR ?ㅽ뻾 # 애플리케이션을 실행합니다.
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -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.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.URI; import java.net.URI;

458
README.md
View File

@@ -1,164 +1,306 @@
# DAP Backend # AX HUB MCP Tool Platform
Spring Boot 기반 DAP 관리자 백엔드 API 서버 및 MCP(Model Context Protocol) Gateway / Tool 분산 서버 프로젝트입니다. AX HUB에서 AI Agent가 업무 Tool을 검색하고 호출할 수 있도록 Gateway와 독립 Tool Pod를 제공하는 멀티 모듈 Spring Boot 프로젝트입니다.
--- ## 1. 현재 구성
## 아키텍처 개요 (Architecture Overview)
DAP Backend는 2개의 주요 애플리케이션으로 분리 운영됩니다:
1. **MCP Gateway (`DapGatewayApplication`)**: 외부 LLM(Claude, GPT 등) 서버의 MCP 통신을 받아, 내부 Tool 서버들로 분배(라우팅)하는 허브 서버이자 관리자 웹(Scaffolder)을 제공하는 통합 서버 (포트: 8081)
2. **MCP Tool (`DapTool*Application`)**: 실제 레거시 시스템(MCI, EAI 등)과 통신하여 비즈니스 로직(결제, 휴가신청 등)을 수행하는 어댑터 서버 (포트: 8082~8085 등 분산 구성 가능)
---
## 환경 (Environment)
| 항목 | 버전 |
|------|------|
| Java | 21 |
| Spring Boot | 4.0.5 |
| Build Tool | Gradle |
| 주요 기술 스택 | MyBatis, Lombok, MapStruct, P6Spy |
| 데이터베이스 | H2 (in-memory, 로컬 개발용) |
| 세션/캐시 저장소 | Redis |
| **장애 격리 / 제어** | **Resilience4j (RateLimiter, CircuitBreaker, Retry)** |
| **메시지 큐** | **Kafka (트래픽 폭주 시 대기열 전환용)** |
---
## ▶ 실행 방법 (How to Run)
### 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` 입력
---
## 🤖 AI Agent 연동 아키텍처 (MCP & Agent Builder)
본 시스템은 **투트랙(Two-Track) AI 연동 아키텍처**를 제공하여 로컬 개발 환경과 프로덕션 환경 모두를 완벽하게 지원합니다.
### 1. 로컬 코딩 AI (Antigravity, Cursor, Claude Desktop 등) 연동
표준 MCP 통신(Stdio)을 요구하는 로컬 AI 에이전트를 위해 자바 기반의 브릿지 스크립트(`McpBridge.java`)를 내장하고 있습니다. 브릿지가 Stdio 요청을 HTTP로 변환하여 로컬 환경의 Gateway(포트: 8281)로 전달합니다.
- **설정 방법**: IDE의 `mcp_config.json` 설정 파일에 아래와 같이 등록합니다.
```json
"mcpServers": {
"dap-gateway": {
"command": "java",
"args": ["C:/절대경로/dap-backend-main/McpBridge.java"]
}
}
```
- **특정 카테고리 툴 필터링**: `McpBridge.java` 내부의 URI 파라미터(`?categoryKey=common`)를 수정하여 원하는 도메인의 툴만 선택적으로 AI에게 학습시킬 수 있습니다.
### 2. 프로덕션 클라우드 AI (Google Cloud Agent Builder 등) 연동
실제 라이브 서비스에서 동작하는 클라우드 Agent Builder는 REST API 기반의 OpenAPI Spec을 요구합니다.
`dap-gateway`는 이미 **Agent Builder 규격의 REST API(`/mcp/api/v1/tools/call`)를 네이티브로 제공**하므로, 별도의 브릿지나 어댑터 없이 Endpoint URL과 Swagger(OpenAPI) 문서만 클라우드 콘솔에 등록하면 즉시 라이브 챗봇/에이전트로 서비스할 수 있습니다.
---
## 비공개 Tool 관리 및 Fallback 연동 (Visibility & Routing)
저희 시스템은 MSA 보안 및 아키텍처 원칙에 따라 Tool의 **레지스트리 등록 여부(라우팅)**와 **API 노출 여부(가시성)**를 완벽히 분리하여 관리합니다.
1. **`visible = false`**:
레지스트리에 정상적으로 등록되어 게이트웨이가 동적으로 라우팅하지만, 클라이언트에게 제공되는 `/tools/list` API 목록에서는 숨겨집니다.
2. **`register = false`**:
내부 레지스트리(Redis)에 툴 정보를 등록하지 않습니다 (외부 레지스트리를 독자적으로 사용할 경우 등).
이 경우 게이트웨이는 `application.properties`의 `mcp.gateway.fallback.routes` 설정을 참조하여 **Fallback 정적 라우팅**을 수행하므로 연동이 100% 보장됩니다.
```java
@McpFunction(
name = "secret_tool",
visible = false, // 목록 숨김 여부 (기본값: true)
register = false // 내부 Redis 등록 여부 (기본값: true)
)
```
---
## 🛡️ 시스템 안정성 및 네트워크 제어 (Resilience & Network)
MSA 및 외부 시스템(MCI) 연동 환경의 안정성을 위해 완벽한 3-Tier 방어 체계를 구축했습니다.
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)
```text ```text
dap-backend-main (Root) MCP Client / AI Agent
├── dap-gateway # MCP 라우팅 허브 서버 (외부 LLM과 통신 및 Tool 분배) |
├── dap-common # 공통 모듈 (Security, Session, Config 등) v
├── dap-tool-core # Tool 공통 기능 (AbstractMcpToolService, Annotation, Scaffolder) Gateway (dap-gateway, 8081)
├── dap-tool-email # [Tool] 이메일 발송 특화 어댑터 모듈 - MCP 연결, Tool Registry, 인증·인가, 라우팅, 공통 화면
├── dap-tool-sms # [Tool] SMS 발송 특화 어댑터 모듈 |
├── dap-tool-payment # [Tool] 결제 비즈니스 어댑터 모듈 (Scaffolded) +-------------------------+
└── dap-tool-other # [Tool] 기타 비즈니스(청구, 계약, 고객, HR 등) 어댑터 모듈 | |
v v
SMS Tool Pod (dap-was-sms, 8082) OTH Tool Pod (dap-was-oth, 8084)
- SMS 업무 Tool - 공통/업무/MCI 연계 Tool
- Tool Manifest - Tool Manifest
- Pod Test Console - Pod Test Console
|
v
MCI / EAI / EIMS 등 레거시 연계 대상
``` ```
*(참고: 기존 단일 모듈 프로젝트에서 마이크로서비스 확장을 위해 모듈별로 분리되었으며, 각 Tool 서버는 독립적으로 확장 및 배포할 수 있습니다.)* Gateway는 외부 MCP 진입점과 Tool 등록·라우팅을 담당하고, 실제 업무 로직과 레거시 호출은 각 Tool Pod가 담당합니다. Tool Pod는 업무/조직 단위로 독립 배포할 수 있습니다.
## 2. 모듈
| 모듈 | 역할 | 기본 포트 |
|---|---|---:|
| `dap-gateway` | MCP Gateway, Registry, 라우팅, Chat·Catalog·Tester·Scaffold 화면 | 8081 |
| `dap-was-lib` | Tool 공통 라이브러리: 어노테이션, Schema, Manifest, MCI/EAI 어댑터, 공통 Web·보안 기능 | - |
| `dap-was-sms` | SMS/알림 업무 Tool Pod | 8082 |
| `dap-was-oth` | 공통/샘플/MCI 연계 업무 Tool Pod | 8084 |
> 기존 `dap-tool-core`, `dap-tool-sms`, `dap-tool-oth` 명칭은 현재 각각 `dap-was-lib`, `dap-was-sms`, `dap-was-oth`로 변경되었습니다.
## 3. 개발 환경
| 항목 | 기준 |
|---|---|
| Java | 21 |
| Spring Boot | 3.5.11 |
| Gradle | 8.14.3 (Wrapper) |
| Spring AI BOM | 1.1.8 |
| 기본 프로필 | `local` |
로컬 환경에서는 민감한 값은 환경 변수로만 주입합니다.
```powershell
$env:SPRING_PROFILES_ACTIVE = 'local'
$env:OPENROUTER_API_KEY = '<발급받은-키>'
```
## 4. 실행
### 4.1 Gradle 로컬 실행
각 애플리케이션은 별도 터미널에서 실행합니다.
```powershell
.\gradlew.bat :dap-gateway:bootRun
.\gradlew.bat :dap-was-sms:bootRun
.\gradlew.bat :dap-was-oth:bootRun
```
### 4.2 Docker Compose 실행
```powershell
docker compose up -d --build
```
| 서비스 | 호스트 URL | 컨테이너 포트 |
|---|---|---:|
| Gateway | http://localhost:8281 | 8081 |
| SMS Tool Pod | http://localhost:8282 | 8082 |
| OTH Tool Pod | http://localhost:8284 | 8084 |
| MCI Mock | http://localhost:8089 | 8080 |
| Dozzle 로그 화면 | http://localhost:8288 | 8080 |
## 5. 화면과 운영 도구
### Gateway 화면
| 화면 | 로컬 URL | 용도 |
|---|---|---|
| 메인 | http://localhost:8081/index.html | 플랫폼 진입 화면 |
| Catalog | http://localhost:8081/catalog.html | 등록 Tool 탐색 |
| Playground | http://localhost:8081/playground.html | 단건 Tool 호출 확인 |
| Chat | http://localhost:8081/chat.html | Agent 기반 대화형 호출 |
| Auto Tester | http://localhost:8081/tester.html | Gateway 기준 다중 Tool 스모크/회귀 테스트 |
| Scaffold | http://localhost:8081/admin/scaffold.html | Pod·Tool 소스 생성 지원 |
Docker Compose로 실행한 경우 Gateway 화면은 포트 `8281`을 사용합니다.
### Tool Pod Test Console
공통 정적 화면인 `tool-test-console.html`은 Tool Pod의 `/tool-manifest`를 읽어, 해당 Pod에 등록된 Tool과 `inputSchema`를 기준으로 요청 JSON을 만들어 직접 실행합니다.
| 대상 Pod | 로컬 URL | Docker Compose URL |
|---|---|---|
| OTH | http://localhost:8084/tool-test-console.html | http://localhost:8284/tool-test-console.html |
| SMS | http://localhost:8082/tool-test-console.html | http://localhost:8282/tool-test-console.html |
사용 방법은 다음과 같습니다.
1. 대상 Tool Pod의 Console에 접속합니다.
2. Tool을 선택하고 `Schema 샘플 채우기`로 요청 JSON을 생성합니다.
3. 업무에 맞는 값으로 보정한 뒤 실행합니다.
4. 재사용할 요청은 `현재 요청 저장`으로 브라우저 `localStorage`에 저장합니다.
### Gateway Auto Tester
`tester.html`은 Gateway에 등록된 Tool을 대상으로 다음 기능을 제공합니다.
- Tool 목록 자동 조회 및 Tool별 실행
- JSON Schema 기반 더미 요청 생성, 사용자 요청 JSON 저장
- 필터된 Tool 일괄 실행 및 실패 Tool 재실행
- 결과 차트, 실시간 실행 로그, CSV 내보내기, 호출 예제 복사
- Tool별 자동 Ping(3초 간격) 및 Stress Test(현재 50건 동시 요청)
> Auto Tester의 더미 요청 성공은 **통신·등록·기본 스키마 확인용 스모크 테스트**입니다. 실제 MCI/EAI 업무 규칙, 권한, 데이터 정합성은 보장하지 않습니다. Stress Test와 Auto Ping은 실제 Tool·레거시 호출을 유발할 수 있으므로 개발/테스트 환경 및 승인된 테스트 데이터에서만 사용합니다.
UI에서 사용하는 Tailwind CSS와 Chart.js는 `dap-gateway/src/main/resources/static/lib`에 포함되어 있어, 화면 라이브러리 로딩을 위해 외부 CDN 연결이 필요하지 않습니다.
## 6. 주요 HTTP API
### Gateway API
| 기능 | Method | 경로 |
|---|---|---|
| 등록 Tool 목록 | GET | `/mcp/api/v1/tools/list` |
| Gateway를 통한 Tool 호출 | POST | `/mcp/api/v1/tools/call` |
| Tool 문서 Markdown | GET | `/mcp/api/v1/tools/docs/markdown` |
| Tool 등록 | POST | `/mcp/api/v1/registry/register` |
| Tool 해제 | POST | `/mcp/api/v1/registry/deregister` |
| Heartbeat | POST | `/mcp/api/v1/registry/heartbeat` |
호출 본문은 JSON-RPC 형식을 사용합니다.
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "cmm_claim_search",
"arguments": {
"claimNo": "CLM2026070100120"
}
},
"id": 1
}
```
### Tool Pod API
| 기능 | Method | 경로 |
|---|---|---|
| 현재 Pod의 Tool Manifest | GET | `/tool-manifest` |
| 현재 Pod의 로컬 Tool 목록 | GET | `/mcp/api/v1/tools/local` |
| 현재 Pod에 직접 Tool 호출 | POST | `/mcp/{toolName}` |
Agent나 외부 클라이언트의 표준 MCP 진입은 Gateway를 사용합니다. Pod 직접 호출은 개발·단위 테스트·Pod Console 용도로 사용합니다.
## 7. Tool 개발 규칙
### 이름
Tool 함수명은 아래 4단계 규칙을 사용합니다.
```text
pod_domain_service_action
예: cmm_claim_search
```
- `pod`: Tool Pod 식별자 (`oth`, `sms` 등)
- `domain`: 업무 도메인 또는 `categoryKey` (`cmm`, `smp` 등)
- `service`: 업무 서비스명
- `action`: 동작 (`search`, `issue`, `send` 등)
### 기본 구현 계층
```text
Request DTO
-> UseCase Interface
-> UseCaseImpl
-> Converter (AI DTO <-> MCI/EAI Interface IO)
-> Client / Adapter
-> Legacy System
```
- Request DTO에는 `@McpValidation`으로 단순 입력 검증을 선언합니다.
- 복잡하거나 조건부 규칙이 필요한 입력은 `tool-schemas/{categoryKey}/...-input-schema.json` 리소스를 사용합니다.
- 응답은 `@McpOutputSchema`로 간단한 설명을 제공하고, 복잡한 응답은 Output Schema 리소스로 명시합니다.
- `null`의 업무 의미, 조건부 필드, 배열 정렬 기준, `hasMore` 여부, 민감 정보 제외 원칙을 Schema 설명에 명확히 씁니다.
### Schema 우선순위
1. `inputSchemaResource` 또는 `outputSchemaResource`가 지정된 경우: 해당 JSON Schema를 사용합니다.
2. 리소스가 없으면: DTO 어노테이션(`@McpValidation`, `@McpOutputSchema`)과 공통 Generator로 생성합니다.
## 8. Manifest와 Registry
각 Tool Pod는 기동 시 `@McpTool`, `@McpFunction` 정보를 읽어 `/tool-manifest`를 제공합니다. Manifest에는 Tool 이름, 설명, endpoint, revision, Input/Output Schema가 포함됩니다.
Gateway는 Registry 등록 및 Heartbeat 정보를 이용해 Tool을 라우팅합니다. Tool Pod가 추가되어도 동일한 Manifest/Registry 규약을 준수하면 Gateway에서 탐색·호출할 수 있습니다.
## 9. MCI/EAI 연계
공통 MCI/EAI 연계 기능과 Glow 기본 설정은 `dap-was-lib`에서 제공합니다. Tool Pod별 애플리케이션 프로필은 각 Pod의 `application*.yml`에서 관리합니다.
```text
dap-was-lib/src/main/resources/glow/application-glow*.yml
dap-was-sms/src/main/resources/application*.yml
dap-was-oth/src/main/resources/application*.yml
```
MCI 호출 주소는 일반적으로 `host + uri`로 구성합니다. 예를 들어 `host=https://dev-ichmci.shinhanlife.co.kr`, `uri=/ntl_mci/clc_rcv`이면 호출 대상은 다음과 같습니다.
```text
https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv
```
`receive-uri`는 Tool이 호출할 주소가 아니라, MCI가 비동기/콜백 방식으로 응답을 전달하도록 별도 계약된 경우 수신에 사용하는 경로입니다.
## 10. 품질 검증
```powershell
.\gradlew.bat :dap-was-lib:test
.\gradlew.bat :dap-was-oth:compileJava
.\gradlew.bat :dap-was-sms:compileJava
.\gradlew.bat validateMcpToolNames
```
배포 전에는 다음을 확인합니다.
- Tool 이름의 전역 중복 여부와 `pod_domain_service_action` 규칙(아래 확인 사항 반영 후)
- Request/Response Schema 및 실제 예제 JSON
- Tool Pod 단위 테스트와 Gateway 경유 호출
- MCI/EAI 오류 코드의 사용자용 응답 매핑
- 민감정보가 요청·응답·로그에 포함되지 않는지
- 승인된 개발/테스트 계정과 데이터만 사용했는지
## 11. 보안과 운영 원칙
- API Key, Runner 등록 토큰, 계정·비밀번호 등 비밀값은 Git에 넣지 않고 환경 변수 또는 Secret Manager로 주입합니다.
- `TESTER-DEV` 같은 테스트 권한은 개발 환경에서만 허용하고, 운영에서는 Tool 단위 최소 권한으로 제한합니다.
- Tool 요청·응답 로그는 마스킹/제외 정책을 거친 값만 남깁니다.
- Trace ID는 요청 흐름 전체를 추적하고, Request ID는 HTTP 호출 단위로 새로 부여합니다.
- Tool Pod는 업무 소유권과 장애 격리 단위에 맞춰 독립 이미지·배포·운영 책임으로 분리합니다.
## 12. 참고 소스 위치
| 기능 | 위치 |
|---|---|
| Tool 공통 Controller | `dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation` |
| Tool 어노테이션 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/annotation` |
| JSON Schema/검증 | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/schema`, `.../validation` |
| Manifest | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/manifest` |
| Tool Scaffold | `dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java` |
| Gateway 라우터 | `dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/McpRouterController.java` |
| OTH Tool | `dap-was-oth/src/main/java` |
| SMS Tool | `dap-was-sms/src/main/java` |
## 13. 모듈명 전환 검증
Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니다.
- `validateMcpToolNames``dap-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다.
- Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send`
- Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다.
- Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다.
## 14. BC-DAB-STD-003 Tool Schema V17 적용
각 Tool의 표준 명세는 Tool Pod별 다음 경로에서 관리합니다.
```text
dap-was-{pod}/src/main/resources/tool-definitions/{categoryKey}/{toolName}.yml
```
Tool 이름은 Pod 정보를 포함하지 않는 `도메인_서비스_행위` 형태의 영문 소문자 snake_case를 사용하며,
정규식 `^[a-z][a-z0-9_]{2,63}$`을 만족해야 합니다. 예: `cmm_claim_search`.
필수 항목은 `name`, `display_name`, `version`, `category_key`, 설명 4개 요소(function, when_to_use,
when_not_to_use, io_limits), `display_description`, 예시 질의 3~10건, 동작 힌트 3개(read_only,
destructive, idempotent), `parameters_schema`입니다. 입력 Schema는 루트 `type: object`, 각 property의
`description`, `additionalProperties: false`를 갖춰야 합니다. 선택 운영 항목은 `tags`,
`legacy_interface_id`, `required_env_keys`, `owner_org`입니다.
기동 시 `tool-definitions/**/*.yml`을 한 번 읽어 이름 기준으로 캐시하고, `@McpTool` 실행 정보와 결합한
동일한 `ToolMetadata``/tool-manifest`, Tool Pod MCP, Gateway MCP에 사용합니다.
입력 Schema 우선순위는 `inputSchemaResource` → V17 `parameters_schema` → DTO 자동 생성이고, 출력은
`outputSchemaResource` → 명시 Output Schema → `@McpOutputSchema` 기반 생성입니다. Output Schema를
명시한 Tool만 최종 응답 검증을 수행합니다.
Scaffold 화면의 `Tool Schema V17 Metadata` 영역에서는 기능 설명, 사용/비사용 조건, 입출력 제한,
표시 설명, 예시 질의, 태그와 소유 조직을 입력합니다. Java 소스와 함께 V17 YAML이 생성되며, 기본값은
배포 전에 업무 담당자가 실제 의미에 맞게 검토해야 합니다.
```powershell
.\gradlew.bat validateMcpToolNames validateToolSchemaV17
```
`bootJar`는 두 검증에 의존하므로 이름 중복, 필수 항목 누락, Java Tool과 YAML 명세 불일치가 있으면
Docker 이미지 생성 전에 빌드가 실패합니다.

View File

@@ -1,51 +1,107 @@
// 루트 Gradle 설정: 모든 Gateway/Tool Pod 모듈이 공유하는 빌드 기준을 정의합니다.
plugins { plugins {
// Java 컴파일, 테스트, JAR 생성 기능을 제공합니다.
id 'java' id 'java'
id 'org.springframework.boot' version '4.0.5' apply false
// 하위 실행 모듈에서 bootRun/bootJar를 사용하기 위한 Spring Boot 플러그인입니다.
// 루트 프로젝트에는 적용하지 않으므로 apply false를 사용합니다.
id 'org.springframework.boot' version '3.5.11' apply false
// Spring Boot / Spring AI BOM에 정의된 라이브러리 버전을 일관되게 적용합니다.
id 'io.spring.dependency-management' version '1.1.6' apply false id 'io.spring.dependency-management' version '1.1.6' apply false
} }
// 모든 모듈이 공유하는 Maven 식별자입니다.
allprojects { allprojects {
group = 'io.shinhanlife' group = 'io.shinhanlife'
version = '0.0.1-SNAPSHOT' version = '0.0.1-SNAPSHOT'
} }
// dap-gateway, dap-was-lib, dap-was-oth, dap-was-sms에 공통 적용합니다.
subprojects { subprojects {
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'io.spring.dependency-management' apply plugin: 'io.spring.dependency-management'
java { java {
// 프로젝트 표준 Java 버전입니다.
sourceCompatibility = '21' sourceCompatibility = '21'
} }
repositories { repositories {
// 현재 공개 정식 라이브러리 저장소입니다.
// 폐쇄망 적용 시 사내 Nexus Proxy URL로 교체합니다.
mavenCentral() mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
} }
dependencyManagement { dependencyManagement {
imports { imports {
// Spring Boot 3.5.11과 호환되는 Spring/Jackson/Tomcat 등의 버전을 관리합니다.
mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
mavenBom "org.springframework.ai:spring-ai-bom:2.0.0"
// Spring AI, MCP, OpenAI 연동 라이브러리의 호환 버전을 1.1.8로 고정합니다.
mavenBom 'org.springframework.ai:spring-ai-bom:1.1.8'
} }
} }
dependencies { dependencies {
// Lombok은 컴파일 시 getter/builder 등 반복 코드를 생성하며 실행 JAR에는 포함하지 않습니다.
compileOnly 'org.projectlombok:lombok:1.18.32' compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' testCompileOnly 'org.projectlombok:lombok:1.18.32'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.32'
// Tool DTO와 MCI 요청/응답 객체 간 Converter 구현체를 컴파일 시 자동 생성합니다.
implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
// 모든 모듈의 JUnit 5 기반 테스트 공통 의존성입니다.
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
// 리플렉션/MCP Schema 생성 시 메서드 파라미터명을 사용할 수 있도록 보존합니다.
options.compilerArgs << '-parameters' options.compilerArgs << '-parameters'
// MapStruct 구현체를 Spring Bean으로 생성해 생성자 주입으로 사용할 수 있게 합니다.
options.compilerArgs << '-Amapstruct.defaultComponentModel=spring' options.compilerArgs << '-Amapstruct.defaultComponentModel=spring'
} }
tasks.withType(Test) { tasks.withType(Test) {
// JUnit 5 테스트 플랫폼을 사용합니다.
useJUnitPlatform() useJUnitPlatform()
} }
} }
// Tool 이름 중복 검사 프로그램이 포함된 공통 라이브러리 모듈입니다.
def toolCoreProject = project(':dap-was-lib')
// 전체 Tool Pod의 @McpTool(name) 중복을 배포 산출물 생성 전에 차단합니다.
tasks.register('validateMcpToolNames', JavaExec) {
group = 'verification'
description = 'Checks duplicate @McpTool names across all Tool modules before packaging.'
// 검사 Runner를 실행하기 전에 dap-was-lib 클래스를 먼저 컴파일합니다.
dependsOn toolCoreProject.tasks.named('classes')
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
mainClass.set('io.shinhanlife.dap.lib.validation.McpToolNameValidationRunner')
args rootProject.projectDir.absolutePath
}
tasks.register('validateToolSchemaV17', JavaExec) {
group = 'verification'
description = 'Validates BC-DAB-STD-003 V17 definitions for every @McpTool.'
dependsOn toolCoreProject.tasks.named('classes')
classpath = toolCoreProject.sourceSets.main.runtimeClasspath
mainClass.set('io.shinhanlife.dap.lib.validation.ToolSchemaV17ValidationRunner')
args rootProject.projectDir.absolutePath
}
subprojects {
// 배포용 Spring Boot JAR 생성 전에 Tool 이름 중복 검증을 강제합니다.
tasks.matching { it.name == 'bootJar' }.configureEach {
dependsOn rootProject.tasks.named('validateMcpToolNames')
dependsOn rootProject.tasks.named('validateToolSchemaV17')
}
}

View File

@@ -1,35 +0,0 @@
plugins {
id 'java-library'
}
dependencies {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-validation'
api 'org.springframework.boot:spring-boot-starter-data-redis'
api 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
api 'io.github.resilience4j:resilience4j-core:2.2.0'
api 'io.github.resilience4j:resilience4j-circuitbreaker:2.2.0'
api 'io.github.resilience4j:resilience4j-ratelimiter'
api 'io.github.resilience4j:resilience4j-retry:2.2.0'
api 'org.springframework.boot:spring-boot-starter-aop:3.3.0'
api 'org.springframework.boot:spring-boot-starter-jdbc'
api 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
api 'com.h2database:h2'
api 'p6spy:p6spy:3.9.1'
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
api "org.mapstruct:mapstruct:1.5.5.Final"
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
api 'com.networknt:json-schema-validator:1.4.0'
api 'org.springframework.kafka:spring-kafka:3.2.0'
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}

View File

@@ -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초당 허용 최대 요청 수
}

View File

@@ -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
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dap.common.session.converter;
import io.shinhanlife.dap.common.session.dto.SessionDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @package io.shinhanlife.dap.common.session.converter
* @className ZtUsacConverter
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public abstract class ZtUsacConverter {
@Mapping(target = "loginDtm", ignore = true)
@Mapping(target = "isManager", ignore = true)
public abstract SessionDto toSessionDto(ZtUsacOutDto dto);
}

View File

@@ -1,51 +0,0 @@
package io.shinhanlife.dap.common.session.domain.model;
import io.shinhanlife.glow.db.dto.AuditInfo;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.session.domain.model
* @className ZtUsacModel
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class ZtUsacModel extends AuditInfo {
/* 인사번호 */
private String prafNo;
/* 인사명 */
private String prafNm;
/* 조직번호 */
private String ognzNo;
/* 이메일주소 */
private String addre;
/* 인사직무코드 */
private String prafOfduCd;
/* 인사직무명 */
private String prafOfduNm;
/* 인사직급코드 */
private String prafOfleCd;
/* 인사직급명 */
private String prafOfleNm;
/* 인사직책코드 */
private String prafDutyCd;
/* 인사직책명 */
private String prafDutyNm;
}

View File

@@ -1,33 +0,0 @@
package io.shinhanlife.dap.common.session.domain.repository;
import io.shinhanlife.glow.GlowMybatisMapper;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
/**
* @package io.shinhanlife.dap.common.session.domain.repository
* @className ZtUsacRepository
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface ZtUsacRepository {
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dap.common.session.domain.service;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
/**
* @package io.shinhanlife.dap.common.session.domain.service
* @className ZtUsacService
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public interface ZtUsacService {
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
}

View File

@@ -1,42 +0,0 @@
package io.shinhanlife.dap.common.session.domain.service.impl;
import io.shinhanlife.dap.common.session.domain.repository.ZtUsacRepository;
import io.shinhanlife.dap.common.session.domain.service.ZtUsacService;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @package io.shinhanlife.dap.common.session.domain.service.impl
* @className ZtUsacServiceImpl
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class ZtUsacServiceImpl implements ZtUsacService {
private final ZtUsacRepository ztUsacRepository;
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
@Override
public ZtUsacOutDto selectZtUsac(ZtUsacInDto dto) {
ZtUsacOutDto result = ztUsacRepository.selectZtUsac(dto);
result.initLists();
return result;
}
}

View File

@@ -1,37 +0,0 @@
package io.shinhanlife.dap.common.session.dto;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.common.session.dto
* @className ZtUsacInDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ZtUsacInDto {
/* 인사번호 */
private String prafNo;
/* 사용여부 */
@Builder.Default
private String puseYn = "Y";
}

View File

@@ -1,85 +0,0 @@
package io.shinhanlife.dap.common.session.dto;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* @package io.shinhanlife.dap.common.session.dto
* @className ZtUsacOutDto
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Setter
public class ZtUsacOutDto {
/* 인사번호 */
private String prafNo;
/* 인사명 */
private String prafNm;
/* 조직번호 */
private String ognzNo;
/* 조직명 */
private String ognzNm;
/* 이메일주소 */
// @GlowSecureField(type = DataSecureType.DECRYPT_DB, direction = SafeDBType.COMM) TODO 방화벽 뚫리면 확인
private String addre;
/* 인사직무코드 */
private String prafOfduCd;
/* 인사직무명 */
private String prafOfduNm;
/* 인사직급코드 */
private String prafOfleCd;
/* 인사직급명 */
private String prafOfleNm;
/* 인사직책코드 */
private String prafDutyCd;
/* 인사직책명 */
private String prafDutyNm;
/* 사용여부 */
private String puseYn;
private String roleNoStrList;
private String roleNmStrList;
private String tgtrPrafNoStrList;
private String tgtrOgnzNoStrList;
private List<String> roleNoList;
private List<String> roleNmList;
private List<String> tgtrPrafNoList;
private List<String> tgtrOgnzNoList;
public void initLists() {
this.roleNoList = convertStrToList(roleNoStrList);
this.roleNmList = convertStrToList(roleNmStrList);
this.tgtrPrafNoList = convertStrToList(tgtrPrafNoStrList);
this.tgtrOgnzNoList = convertStrToList(tgtrOgnzNoStrList);
}
private List<String> convertStrToList(String str) {
if (str == null || str.isEmpty()) {
return new ArrayList<>();
}
return Arrays.asList(str.split(","));
}
}

View File

@@ -1,91 +0,0 @@
package io.shinhanlife.dap.common.session.presentation;
import io.micrometer.common.util.StringUtils;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.BizException;
import io.shinhanlife.glow.GlowControllerId;
import io.shinhanlife.glow.ResponseUtil;
import io.shinhanlife.dap.common.session.converter.ZtUsacConverter;
import io.shinhanlife.dap.common.session.domain.service.ZtUsacService;
import io.shinhanlife.dap.common.session.dto.SessionDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.common.session.dto.ZtUsacOutDto;
import io.shinhanlife.dap.common.session.presentation.io.SsoResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
/**
* @package io.shinhanlife.dap.common.session.presentation
* @className SsoRestController
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/sso")
public class SsoRestController {
private static final String NLS_LOGIN_URL = "";
private final ZtUsacService ztUsacService;
private final ZtUsacConverter ztUsacConverter;
/**
* sso 연동 전 임시 로그인
*
* @param request
* @param response
* @param session
* @param <T>
* @return
*/
@GlowControllerId("tempLogin")
@PostMapping("/tempLogin")
public <T> ResponseEntity<BaseResponse<SsoResponse>> tempLogin(HttpServletRequest request, HttpServletResponse response,
HttpSession session, @RequestBody SessionDto requestDto) {
try {
if (StringUtils.isEmpty(requestDto.getPrafNo())) {
throw new NotFoundException("SSO >> not found sso id");
}
// DB 유저 가져오기
ZtUsacOutDto ztUsacOutDto = ztUsacService.selectZtUsac(ZtUsacInDto.builder().prafNo(requestDto.getPrafNo()).puseYn("Y")
.build());
if (Objects.isNull(ztUsacOutDto) || StringUtils.isEmpty(ztUsacOutDto.getPrafNo())) {
throw new BizException("SSO >> not found UserInfo >> retCode:");
}
SessionDto sessionDto = ztUsacConverter.toSessionDto(ztUsacOutDto);
sessionDto.setLoginDtm(); // 로그인 시점 세팅
session.setAttribute("userInfo", sessionDto);
return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build());
} catch (Exception e) {
log.error("로그인 실패", e);
session.invalidate();
}
return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build());
}
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.dap.common.session.presentation.io;
import io.shinhanlife.dap.common.session.dto.SessionDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.common.session.presentation.io
* @className SsoResponse
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SsoResponse {
private String retCode;
private SessionDto userInfo;
private String redirectUrl;
}

View File

@@ -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();
}
}

View File

@@ -1,108 +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.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ToolSourceUpdater {
public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception {
// 1. Find all *Service.java files in axhub-tool-* directories
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
List<Path> javaFiles;
try (Stream<Path> paths = Files.walk(rootDir)) {
javaFiles = paths
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith("Service.java"))
.filter(p -> p.toString().contains("axhub-tool-"))
.collect(Collectors.toList());
}
Path targetFile = null;
String content = null;
// 2. Find the specific file for the tool
String functionName = toolName;
if (toolName.contains("_")) {
functionName = toolName.substring(toolName.indexOf("_") + 1);
}
Pattern namePattern = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"", Pattern.DOTALL);
Pattern namePattern2 = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(functionName) + "\"", Pattern.DOTALL);
for (Path path : javaFiles) {
String text = Files.readString(path);
if (namePattern.matcher(text).find()) {
targetFile = path;
content = text;
break;
} else if (namePattern2.matcher(text).find()) {
targetFile = path;
content = text;
toolName = functionName; // Use baseName for subsequent replacements
break;
}
}
if (targetFile == null) {
throw new Exception("소스 코드를 찾을 수 없습니다: " + toolName);
}
// 3. Update @McpTool group
if (domainGroup != null && !domainGroup.trim().isEmpty()) {
Pattern groupPattern = Pattern.compile("(@McpTool\\s*\\([^)]*group\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL);
Matcher groupMatcher = groupPattern.matcher(content);
if (groupMatcher.find()) {
content = groupMatcher.replaceFirst("$1" + domainGroup + "$3");
}
}
// 4. Update @McpFunction description
if (description != null) {
Pattern funcPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*description\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL);
Matcher funcMatcher = funcPattern.matcher(content);
if (funcMatcher.find()) {
content = funcMatcher.replaceFirst("$1" + description.replace("\\", "\\\\").replace("$", "\\\\$") + "$3");
}
}
// 5. Update register flag
Pattern regPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*register\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL);
Matcher regMatcher = regPattern.matcher(content);
if (regMatcher.find()) {
content = regMatcher.replaceFirst("$1" + register + "$3");
} else {
Pattern addRegPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL);
Matcher addRegMatcher = addRegPattern.matcher(content);
if (addRegMatcher.find()) {
content = addRegMatcher.replaceFirst("$1, register = " + register);
}
}
// 5.5 Update requiresApproval flag
if (requiresApproval != null) {
Pattern appPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*requiresApproval\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL);
Matcher appMatcher = appPattern.matcher(content);
if (appMatcher.find()) {
content = appMatcher.replaceFirst("$1" + requiresApproval + "$3");
} else {
Pattern addAppPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL);
Matcher addAppMatcher = addAppPattern.matcher(content);
if (addAppMatcher.find()) {
content = addAppMatcher.replaceFirst("$1, requiresApproval = " + requiresApproval);
}
}
}
// 6. Write back to file
Files.writeString(targetFile, content);
}
}

View File

@@ -1,20 +0,0 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className BizException
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
public class BizException extends RuntimeException {
public BizException(String s) {
}
}

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.glow;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlowLogTarget {
Target[] value() default {};
/**
* @package io.shinhanlife.glow
* @className Target
* @description AX HUB 시스템 처리 클래스
* @author 김형식
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성
*
* </pre>
*/
enum Target {
FILE, CONSOLE
}
}

View File

@@ -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 FROM eclipse-temurin:21-jre-alpine
WORKDIR /app WORKDIR /app
RUN apk add --no-cache tzdata RUN apk add --no-cache tzdata
ENV TZ=Asia/Seoul ENV TZ=Asia/Seoul
COPY --from=builder /app/dap-gateway/build/libs/*-SNAPSHOT.jar app.jar COPY dap-gateway/build/libs/*-SNAPSHOT.jar app.jar
EXPOSE 8081 EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"] ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,38 +1,37 @@
plugins { plugins {
// Gateway를 독립 실행 가능한 Spring Boot JAR로 생성합니다.
id 'org.springframework.boot' id 'org.springframework.boot'
} }
dependencies { dependencies {
implementation project(':dap-common') // Tool Registry, MCP 공통 처리, 보안/Schema 유틸리티를 공통 라이브러리에서 가져옵니다.
implementation project(':dap-was-lib')
// Gateway REST API와 관리 화면의 HTTP 요청을 처리합니다.
implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-web'
// 등록된 Excel 양식을 보존하면서 Tool 문서를 생성합니다.
implementation 'org.apache.poi:poi-ooxml:5.5.1'
// Tool Registry 및 분산 캐시 연동에 사용합니다.
implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Spring AI MCP Server // Tool/Registry 관련 DB 조회와 MyBatis Mapper 실행에 사용합니다.
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
// MyBatis & DB
implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
// Gateway가 /mcp Endpoint를 MCP Server로 노출하도록 지원합니다.
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
// ChatClient를 통해 OpenAI/OpenRouter 호환 모델을 호출합니다.
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
// 로컬 개발·테스트용 인메모리 DB입니다. 운영 DB에는 사용하지 않습니다.
runtimeOnly 'com.h2database:h2' runtimeOnly 'com.h2database:h2'
// SQL 로그를 확인하기 위한 JDBC 프록시입니다.
implementation 'p6spy:p6spy:3.9.1' implementation 'p6spy:p6spy:3.9.1'
// MapStruct // Gateway REST API 문서와 Swagger UI를 제공합니다.
implementation "org.mapstruct:mapstruct:1.5.5.Final"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
// implementation 'com.networknt:json-schema-validator:1.4.0' // Spring AI 내장 버전과 충돌 방지를 위해 주석 처리
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
} }

View File

@@ -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);
}
}

View File

@@ -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");
}
}

View File

@@ -1,90 +0,0 @@
package io.shinhanlife.dap.biz.mcp.gateway.presentation;
import io.shinhanlife.dap.common.util.PodScaffolder;
import io.shinhanlife.dap.common.util.ToolScaffolder;
import io.shinhanlife.dap.common.util.ToolSourceUpdater;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/scaffold")
public class ScaffoldingController {
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-tool-other");
if (!moduleName.startsWith("dap-tool-")) moduleName = "dap-tool-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-tool-", "").replace("-", "");
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"));
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool")
public String scaffoldTool(@RequestBody Map<String, String> req) {
try {
String baseName = req.get("baseName");
String interfaceId = req.get("interfaceId");
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 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"));
return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool/update")
public String updateTool(@RequestBody Map<String, String> req) {
try {
String toolName = req.get("toolName");
String domainGroup = req.getOrDefault("categoryKey", req.get("domainGroup"));
String description = req.get("description");
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
Boolean requiresApproval = req.containsKey("requiresApproval") ? Boolean.parseBoolean(req.get("requiresApproval")) : null;
ToolSourceUpdater.updateToolSource(toolName, domainGroup, description, register, requiresApproval);
return "성공";
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@GetMapping("/modules")
public List<String> listModules() {
try {
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
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"));
if (files == null) return List.of("dap-tool-other");
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-tool-other");
}
}
}

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.aop; package io.shinhanlife.dap.mcg.aop;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
@@ -8,16 +8,16 @@ import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch; import org.springframework.util.StopWatch;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.aop * @package io.shinhanlife.dap.mcg.aop
* @className GatewayLoggingAspect * @className GatewayLoggingAspect
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */
@@ -27,7 +27,7 @@ import org.springframework.util.StopWatch;
public class GatewayLoggingAspect { public class GatewayLoggingAspect {
// gateway의 controller 패키지 하위의 모든 클래스/메서드 실행 작동 // 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 { public Object logGatewayExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
String targetMethod = joinPoint.getSignature().toShortString(); String targetMethod = joinPoint.getSignature().toShortString();
StopWatch stopWatch = new StopWatch(); StopWatch stopWatch = new StopWatch();

View File

@@ -1,8 +1,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 com.fasterxml.jackson.databind.JsonNode;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -17,11 +31,8 @@ import org.springframework.stereotype.Service;
public class AuditLogService { public class AuditLogService {
private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT"); private static final Logger audit = LoggerFactory.getLogger("MCP_AUDIT");
private final McpGatewayProperties properties; private final McpGatewayProperties properties;
private final SensitiveDataMasker masker; public AuditLogService(McpGatewayProperties properties) {
public AuditLogService(McpGatewayProperties properties, SensitiveDataMasker masker) {
this.properties = properties; this.properties = properties;
this.masker = masker;
} }
/** /**
@@ -32,7 +43,7 @@ public class AuditLogService {
return; return;
} }
audit.info("event=tool_started requestId={} agentId={} userId={} clientAddress={} tool={} arguments={}", 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);
} }
/** /**

View File

@@ -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.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.config; package io.shinhanlife.dap.mcg.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -8,16 +8,16 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.config * @package io.shinhanlife.dap.mcg.config
* @className GatewayFallbackProperties * @className GatewayFallbackProperties
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -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.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -10,7 +25,7 @@ import java.util.stream.Collectors;
@Component @Component
@ConfigurationProperties(prefix = "mcp") @ConfigurationProperties(prefix = "mcp")
/** /**
* application.properties mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다. * application.yml mcp.* 설정을 Java 코드에서 사용하기 위한 설정 클래스입니다.
* *
* API Key, 허용 Tool, 감사 로그, Redis Trace, Retry, Circuit Breaker, * API Key, 허용 Tool, 감사 로그, Redis Trace, Retry, Circuit Breaker,
* EIMS/Tool 서버 주소 같은 MCP Gateway 운영 설정을 곳에서 관리합니다. * EIMS/Tool 서버 주소 같은 MCP Gateway 운영 설정을 곳에서 관리합니다.

View File

@@ -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.mcc.dto.ToolMetadata;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
@@ -10,16 +10,16 @@ import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.config * @package io.shinhanlife.dap.mcg.config
* @className RedisConfig * @className RedisConfig
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -0,0 +1,11 @@
package io.shinhanlife.dap.mcg.document;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
public record DocumentGenerationRequest(
ToolMetadata tool,
String version,
boolean includeProgram,
boolean includeProcess,
boolean includeRevision) {
}

View File

@@ -0,0 +1,56 @@
package io.shinhanlife.dap.mcg.document;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.springframework.http.CacheControl;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
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;
@RestController
@RequestMapping("/mcp/api/v1/admin/documents")
public class DocumentGeneratorController {
private static final MediaType XLSX = MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
private final DocumentGeneratorService documentGeneratorService;
public DocumentGeneratorController(DocumentGeneratorService documentGeneratorService) {
this.documentGeneratorService = documentGeneratorService;
}
@PostMapping(value = "/program", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public ResponseEntity<byte[]> generateProgram(@RequestBody DocumentGenerationRequest request) {
return download(documentGeneratorService.generateProgram(request));
}
@PostMapping(value = "/interface", produces = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
public ResponseEntity<byte[]> generateInterface(@RequestBody DocumentGenerationRequest request) {
return download(documentGeneratorService.generateInterface(request));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> invalidRequest(IllegalArgumentException exception) {
return ResponseEntity.badRequest().body(Map.of("error", exception.getMessage()));
}
private ResponseEntity<byte[]> download(GeneratedDocument document) {
ContentDisposition disposition = ContentDisposition.attachment()
.filename(document.fileName(), StandardCharsets.UTF_8)
.build();
return ResponseEntity.ok()
.contentType(XLSX)
.contentLength(document.content().length)
.cacheControl(CacheControl.noStore())
.header(HttpHeaders.CONTENT_DISPOSITION, disposition.toString())
.body(document.content());
}
}

View File

@@ -0,0 +1,534 @@
package io.shinhanlife.dap.mcg.document;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Clock;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.springframework.core.io.ClassPathResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DocumentGeneratorService {
static final String PROGRAM_TEMPLATE = "document-templates/excel/program-definition-template.xlsx";
static final String INTERFACE_TEMPLATE = "document-templates/excel/interface-definition-template.xlsx";
private static final DateTimeFormatter FILE_DATE = DateTimeFormatter.BASIC_ISO_DATE;
private static final DateTimeFormatter DISPLAY_DATE = DateTimeFormatter.ISO_LOCAL_DATE;
private static final Pattern PATTERN_LENGTH = Pattern.compile("\\\\d\\{(\\d+)}");
private static final int INTERFACE_FIRST_ROW = 8;
private static final int INTERFACE_LAST_ROW = 30;
private final ObjectMapper objectMapper;
private final Clock clock;
@Autowired
public DocumentGeneratorService(ObjectMapper objectMapper) {
this(objectMapper, Clock.system(ZoneId.of("Asia/Seoul")));
}
DocumentGeneratorService(ObjectMapper objectMapper, Clock clock) {
this.objectMapper = objectMapper;
this.clock = clock;
}
public GeneratedDocument generateProgram(DocumentGenerationRequest request) {
ToolMetadata tool = validate(request);
if (!request.includeProgram() && !request.includeProcess() && !request.includeRevision()) {
throw new IllegalArgumentException("프로그램 문서에서 한 개 이상의 시트를 선택하세요.");
}
String version = normalizeVersion(request.version());
LocalDate today = LocalDate.now(clock);
try (InputStream input = resource(PROGRAM_TEMPLATE);
Workbook workbook = WorkbookFactory.create(input)) {
Sheet programSheet = requiredSheet(workbook, "프로그램정의서");
Sheet designTemplate = requiredSheet(workbook, "입출력정의");
Sheet revisionSheet = request.includeRevision()
? workbook.cloneSheet(workbook.getSheetIndex(designTemplate))
: null;
if (request.includeProgram()) {
populateProgramSheet(programSheet, tool, version, today);
} else {
workbook.removeSheetAt(workbook.getSheetIndex(programSheet));
}
if (request.includeProcess()) {
workbook.setSheetName(workbook.getSheetIndex(designTemplate), "처리설계");
populateProcessSheet(designTemplate, tool);
} else {
workbook.removeSheetAt(workbook.getSheetIndex(designTemplate));
}
if (revisionSheet != null) {
workbook.setSheetName(workbook.getSheetIndex(revisionSheet), "개정이력");
populateRevisionSheet(revisionSheet, version, today);
}
workbook.setActiveSheet(0);
return output(workbook, programFileName(tool, version, today));
} catch (IOException exception) {
throw new IllegalStateException("프로그램정의서 Excel 생성에 실패했습니다.", exception);
}
}
public GeneratedDocument generateInterface(DocumentGenerationRequest request) {
ToolMetadata tool = validate(request);
String version = normalizeVersion(request.version());
LocalDate today = LocalDate.now(clock);
try (InputStream input = resource(INTERFACE_TEMPLATE);
Workbook workbook = WorkbookFactory.create(input)) {
populateInterfaceSheet(requiredSheet(workbook, "Request In"), tool, false);
populateInterfaceSheet(requiredSheet(workbook, "Response Out"), tool, true);
workbook.setActiveSheet(0);
return output(workbook, interfaceFileName(tool, version, today));
} catch (IOException exception) {
throw new IllegalStateException("인터페이스정의서 Excel 생성에 실패했습니다.", exception);
}
}
private void populateProgramSheet(Sheet sheet, ToolMetadata tool, String version, LocalDate today) {
String label = label(tool);
set(sheet, "B4", documentId(tool));
set(sheet, "E4", version);
set(sheet, "H4", DISPLAY_DATE.format(today));
set(sheet, "B5", label);
set(sheet, "E5", value(tool.getName()));
set(sheet, "H5", "생성 완료");
set(sheet, "B6", value(tool.getCategoryKey()));
set(sheet, "E6", "AX HUB MCP Gateway");
set(sheet, "H6", "자동 생성");
set(sheet, "B9", defaultValue(tool.getDescription(), "설명이 등록되지 않은 Tool입니다."));
set(sheet, "B10", label);
set(sheet, "E10", tool.getOperationType() == null ? "-" : tool.getOperationType().name());
set(sheet, "H10", Boolean.FALSE.equals(tool.getVisible()) ? "비공개" : "공개");
set(sheet, "B11", schemaFields(tool.getParametersSchema()).size()
+ "개 파라미터가 Tool JSON 스키마에서 자동 매핑되었습니다.");
set(sheet, "B14", value(tool.getCategoryKey()));
set(sheet, "E14", value(tool.getPodUrl()));
set(sheet, "H14", value(tool.getIntegrationType()));
set(sheet, "B15", value(tool.getEndpoint()));
set(sheet, "E15", value(tool.getMciServiceId()));
set(sheet, "H15", formatTimeout(tool.getTimeoutMillis()));
set(sheet, "B16", yesNo(tool.getRequiresApproval()));
set(sheet, "E16", yesNo(tool.getReadOnlyHint()));
set(sheet, "H16", yesNo(tool.getIdempotentHint()));
set(sheet, "B21", label);
set(sheet, "C21", "Tool 입력 스키마에 따라 요청 파라미터를 검증합니다.");
set(sheet, "B22", defaultValue(tool.getIntegrationType(), "REST"));
set(sheet, "H22", value(tool.getMciServiceId()));
set(sheet, "A26", "※ ToolMetadata를 기준으로 자동 생성된 문서입니다. 업무 규칙과 승인 정보는 담당자 검토가 필요합니다.");
}
private void populateProcessSheet(Sheet sheet, ToolMetadata tool) {
CellStyle title = style(sheet, "A1");
CellStyle section = style(sheet, "A2");
CellStyle header = style(sheet, "A3");
CellStyle data = style(sheet, "A4");
CellStyle note = style(sheet, "A9");
resetSheet(sheet, 9, 9);
mergeSet(sheet, "A1:I1", "처리설계", title);
mergeSet(sheet, "A2:I2", label(tool) + " · 공통 처리 절차", section);
setStyled(sheet, 2, 0, "단계", header);
setStyled(sheet, 2, 1, "처리 주체", header);
mergeSet(sheet, "C3:F3", "처리 내용", header);
mergeSet(sheet, "G3:H3", "성공 조건", header);
setStyled(sheet, 2, 8, "비고", header);
List<List<String>> steps = List.of(
List.of("1", "Gateway", "호출자 인증과 Tool 실행 권한을 확인합니다.", "권한 검증 성공", "공통 처리"),
List.of("2", label(tool), "Tool 입력 스키마에 따라 요청 파라미터를 검증합니다.", "스키마 검증 성공", "자동 생성"),
List.of("3", defaultValue(tool.getIntegrationType(), "REST"), "등록된 엔드포인트 또는 서비스 ID로 대상 시스템을 호출합니다.", "정상 응답 수신", value(tool.getMciServiceId())),
List.of("4", "Gateway", "응답을 MCP 표준 결과로 변환하고 정책을 검사합니다.", "응답 정책 통과", "응답 정책"),
List.of("5", "Gateway", "감사 로그를 기록하고 호출자에게 결과를 반환합니다.", "응답 전송 완료", "추적 ID 포함")
);
for (int index = 0; index < steps.size(); index++) {
int row = 3 + index;
List<String> step = steps.get(index);
setStyled(sheet, row, 0, step.get(0), data);
setStyled(sheet, row, 1, step.get(1), data);
mergeSet(sheet, "C" + (row + 1) + ":F" + (row + 1), step.get(2), data);
mergeSet(sheet, "G" + (row + 1) + ":H" + (row + 1), step.get(3), data);
setStyled(sheet, row, 8, step.get(4), data);
sheet.getRow(row).setHeightInPoints(38);
}
mergeSet(sheet, "A9:I9", "※ 공통 처리 흐름은 ToolMetadata와 Gateway 정책을 기준으로 자동 작성되었습니다.", note);
}
private void populateRevisionSheet(Sheet sheet, String version, LocalDate today) {
CellStyle title = style(sheet, "A1");
CellStyle section = style(sheet, "A2");
CellStyle header = style(sheet, "A3");
CellStyle data = style(sheet, "A4");
CellStyle note = style(sheet, "A9");
resetSheet(sheet, 9, 9);
mergeSet(sheet, "A1:I1", "개정이력", title);
mergeSet(sheet, "A2:I2", "문서 버전 및 변경 내역", section);
setStyled(sheet, 2, 0, "버전", header);
setStyled(sheet, 2, 1, "작성일", header);
mergeSet(sheet, "C3:D3", "작성자", header);
mergeSet(sheet, "E3:H3", "변경 내용", header);
setStyled(sheet, 2, 8, "비고", header);
setStyled(sheet, 3, 0, version, data);
setStyled(sheet, 3, 1, DISPLAY_DATE.format(today), data);
mergeSet(sheet, "C4:D4", "Document Generator", data);
mergeSet(sheet, "E4:H4", "ToolMetadata 기준 최초 생성", data);
setStyled(sheet, 3, 8, "자동 생성", data);
mergeSet(sheet, "A6:I6", "※ 배포 전 담당자의 최종 검토가 필요합니다.", note);
}
private void populateInterfaceSheet(Sheet sheet, ToolMetadata tool, boolean response) {
String label = label(tool);
String interfaceId = interfaceId(tool);
set(sheet, "A1", label + " 인터페이스 설계서");
set(sheet, "C2", label);
set(sheet, "C3", defaultValue(tool.getDescription(), "설명이 등록되지 않은 Tool입니다."));
set(sheet, "D4", value(tool.getEndpoint()));
set(sheet, "D5", "운영 URL 확인 필요");
set(sheet, "A7", interfaceId);
set(sheet, "B7", response ? "데이터 수신시스템 · 응답 (Response Out)" : "데이터 송신시스템 · 요청 (Request In)");
clearInterfaceRows(sheet);
if (response) {
writeInterfaceField(sheet, INTERFACE_FIRST_ROW, interfaceId, "AX HUB\nMCP Gateway", "Body",
interfaceId + "_O", new SchemaField("resultData", "object", "결과 데이터", "-", true, "", false));
set(sheet, "C33", prettyJson(Map.of("resultData", Map.of("status", "SUCCESS"))));
} else {
List<SchemaField> fields = schemaFields(tool.getParametersSchema());
if (fields.size() > INTERFACE_LAST_ROW - INTERFACE_FIRST_ROW + 1) {
throw new IllegalArgumentException("인터페이스 양식은 최대 23개 요청 필드를 지원합니다.");
}
for (int index = 0; index < fields.size(); index++) {
writeInterfaceField(sheet, INTERFACE_FIRST_ROW + index, interfaceId,
defaultValue(tool.getIntegrationType(), "Tool"), "Body", interfaceId + "_I", fields.get(index));
}
set(sheet, "C33", prettyJson(exampleFromSchema(tool.getParametersSchema())));
}
set(sheet, "A34", "※ ToolMetadata를 기준으로 자동 생성된 검토용 문서입니다.");
}
private void writeInterfaceField(Sheet sheet, int rowIndex, String interfaceId, String system, String level,
String store, SchemaField field) {
if (rowIndex == INTERFACE_FIRST_ROW) {
setStyled(sheet, rowIndex, 0, interfaceId, style(sheet, "A9"));
setStyled(sheet, rowIndex, 1, system, style(sheet, "B9"));
setStyled(sheet, rowIndex, 2, level, style(sheet, "C9"));
}
setStyled(sheet, rowIndex, 3, store, style(sheet, "D9"));
setStyled(sheet, rowIndex, 4, defaultValue(field.description(), field.path()), style(sheet, "E9"));
setStyled(sheet, rowIndex, 5, field.path(), style(sheet, "F9"));
setStyled(sheet, rowIndex, 6, field.type(), style(sheet, "G9"));
setStyled(sheet, rowIndex, 7, field.length(), style(sheet, "H9"));
setStyled(sheet, rowIndex, 8, "", style(sheet, "I9"));
setStyled(sheet, rowIndex, 9, field.coded() ? "Y" : "N", style(sheet, "J9"));
String note = (field.required() ? "필수 · " : "") + "ToolMetadata";
if (!field.example().isBlank()) {
note += " · 예시: " + field.example();
}
setStyled(sheet, rowIndex, 10, note, style(sheet, "K9"));
}
private void clearInterfaceRows(Sheet sheet) {
for (int rowIndex = INTERFACE_FIRST_ROW; rowIndex <= INTERFACE_LAST_ROW; rowIndex++) {
for (int column = 3; column <= 10; column++) {
setStyled(sheet, rowIndex, column, "", style(sheet, "D9"));
}
}
set(sheet, "A9", "");
set(sheet, "B9", "");
set(sheet, "C9", "");
}
private List<SchemaField> schemaFields(Map<String, Object> schema) {
List<SchemaField> fields = new ArrayList<>();
collectSchemaFields(schema, "", Set.of(), fields);
return fields;
}
@SuppressWarnings("unchecked")
private void collectSchemaFields(Map<String, Object> schema, String prefix, Set<String> inheritedRequired,
List<SchemaField> fields) {
if (schema == null) {
return;
}
Object requiredValue = schema.get("required");
Set<String> required = requiredValue instanceof Collection<?> values
? values.stream().map(String::valueOf).collect(java.util.stream.Collectors.toSet())
: inheritedRequired;
Object propertiesValue = schema.get("properties");
if (!(propertiesValue instanceof Map<?, ?> properties)) {
return;
}
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String name = String.valueOf(entry.getKey());
if (!(entry.getValue() instanceof Map<?, ?> rawNode)) {
continue;
}
Map<String, Object> node = (Map<String, Object>) rawNode;
String path = prefix.isBlank() ? name : prefix + "." + name;
String type = String.valueOf(node.getOrDefault("type", "string"));
String description = value(node.get("description"));
String length = schemaLength(node);
String example = schemaExample(node);
boolean coded = node.get("enum") instanceof Collection<?> values && !values.isEmpty();
fields.add(new SchemaField(path, type, description, length, required.contains(name), example, coded));
if ("object".equals(type)) {
collectSchemaFields(node, path, Set.of(), fields);
} else if ("array".equals(type) && node.get("items") instanceof Map<?, ?> items) {
collectSchemaFields((Map<String, Object>) items, path + "[]", Set.of(), fields);
}
}
}
private String schemaLength(Map<String, Object> node) {
Object length = node.get("maxLength");
if (length == null) {
length = node.get("length");
}
if (length != null) {
return String.valueOf(length);
}
Object pattern = node.get("pattern");
if (pattern != null) {
Matcher matcher = PATTERN_LENGTH.matcher(String.valueOf(pattern));
if (matcher.find()) {
return matcher.group(1);
}
}
return "-";
}
private String schemaExample(Map<String, Object> node) {
Object example = node.get("example");
if (example == null && node.get("examples") instanceof List<?> examples && !examples.isEmpty()) {
example = examples.get(0);
}
if (example == null) {
example = node.get("default");
}
return value(example);
}
@SuppressWarnings("unchecked")
private Object exampleFromSchema(Map<String, Object> schema) {
if (schema == null) {
return Map.of();
}
Object type = schema.get("type");
if ("object".equals(type) || schema.get("properties") instanceof Map<?, ?>) {
Map<String, Object> result = new LinkedHashMap<>();
Object propertiesValue = schema.get("properties");
if (propertiesValue instanceof Map<?, ?> properties) {
for (Map.Entry<?, ?> entry : properties.entrySet()) {
if (entry.getValue() instanceof Map<?, ?> node) {
result.put(String.valueOf(entry.getKey()), exampleFromSchema((Map<String, Object>) node));
}
}
}
return result;
}
Object example = schema.get("example");
if (example == null && schema.get("examples") instanceof List<?> examples && !examples.isEmpty()) {
example = examples.get(0);
}
if (example == null) {
example = schema.get("default");
}
if (example != null) {
return example;
}
return switch (String.valueOf(type)) {
case "integer", "number" -> 0;
case "boolean" -> false;
case "array" -> schema.get("items") instanceof Map<?, ?> items
? List.of(exampleFromSchema((Map<String, Object>) items)) : List.of();
default -> "<value>";
};
}
private String prettyJson(Object value) {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);
} catch (JsonProcessingException exception) {
throw new IllegalStateException("JSON 예시 생성에 실패했습니다.", exception);
}
}
private GeneratedDocument output(Workbook workbook, String fileName) throws IOException {
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
workbook.write(output);
return new GeneratedDocument(fileName, output.toByteArray());
}
}
private InputStream resource(String path) throws IOException {
return new ClassPathResource(path).getInputStream();
}
private ToolMetadata validate(DocumentGenerationRequest request) {
if (request == null || request.tool() == null) {
throw new IllegalArgumentException("ToolMetadata가 필요합니다.");
}
ToolMetadata tool = request.tool();
if (isBlank(tool.getUid()) && isBlank(tool.getName())) {
throw new IllegalArgumentException("Tool UID 또는 Tool 이름이 필요합니다.");
}
return tool;
}
private String programFileName(ToolMetadata tool, String version, LocalDate today) {
return safeFilename(label(tool)) + "_프로그램정의서_v" + version + "_" + FILE_DATE.format(today) + ".xlsx";
}
private String interfaceFileName(ToolMetadata tool, String version, LocalDate today) {
return safeFilename(label(tool)) + "_인터페이스정의서_v" + version + "_" + FILE_DATE.format(today) + ".xlsx";
}
private String normalizeVersion(String version) {
String normalized = isBlank(version) ? "1.0" : version.trim().replaceFirst("^[vV]", "");
if (!normalized.matches("[0-9A-Za-z._-]+")) {
throw new IllegalArgumentException("버전은 영문, 숫자, 점, 밑줄, 하이픈만 사용할 수 있습니다.");
}
return normalized;
}
private String safeFilename(String value) {
String result = defaultValue(value, "tool").replaceAll("[\\\\/:*?\"<>|]", "_").trim();
return result.isEmpty() ? "tool" : result;
}
private String label(ToolMetadata tool) {
return defaultValue(tool.getDisplayName(), defaultValue(tool.getName(), tool.getUid()));
}
private String documentId(ToolMetadata tool) {
String category = defaultValue(tool.getCategoryKey(), "ETC").toUpperCase(Locale.ROOT);
String uid = defaultValue(tool.getUid(), tool.getName()).replaceAll("[^0-9A-Za-z]", "");
uid = uid.length() > 7 ? uid.substring(0, 7) : uid;
return "AXHUB-FS-" + category + "-" + uid.toUpperCase(Locale.ROOT);
}
private String interfaceId(ToolMetadata tool) {
int hash = Objects.hash(tool.getUid(), tool.getName());
return "AXHUB" + String.format(Locale.ROOT, "%05d", Math.floorMod(hash, 100_000));
}
private String formatTimeout(Long timeout) {
return timeout == null ? "-" : String.format(Locale.ROOT, "%,d ms", timeout);
}
private String yesNo(Boolean value) {
return Boolean.TRUE.equals(value) ? "Y" : "N";
}
private Sheet requiredSheet(Workbook workbook, String name) {
Sheet sheet = workbook.getSheet(name);
if (sheet == null) {
throw new IllegalStateException("Excel 템플릿에 '" + name + "' 시트가 없습니다.");
}
return sheet;
}
private void resetSheet(Sheet sheet, int rows, int columns) {
for (int index = sheet.getNumMergedRegions() - 1; index >= 0; index--) {
sheet.removeMergedRegion(index);
}
for (int rowIndex = 0; rowIndex < rows; rowIndex++) {
Row row = row(sheet, rowIndex);
for (int column = 0; column < columns; column++) {
cell(row, column).setBlank();
}
}
}
private void mergeSet(Sheet sheet, String range, String value, CellStyle style) {
CellRangeAddress address = CellRangeAddress.valueOf(range);
sheet.addMergedRegion(address);
for (int rowIndex = address.getFirstRow(); rowIndex <= address.getLastRow(); rowIndex++) {
for (int column = address.getFirstColumn(); column <= address.getLastColumn(); column++) {
Cell target = cell(row(sheet, rowIndex), column);
target.setCellStyle(style);
if (rowIndex == address.getFirstRow() && column == address.getFirstColumn()) {
target.setCellValue(value);
}
}
}
}
private void set(Sheet sheet, String reference, String value) {
CellReference cellReference = new CellReference(reference);
Cell target = cell(row(sheet, cellReference.getRow()), cellReference.getCol());
target.setBlank();
target.setCellValue(defaultValue(value, ""));
}
private void setStyled(Sheet sheet, int rowIndex, int column, String value, CellStyle style) {
Cell target = cell(row(sheet, rowIndex), column);
target.setBlank();
target.setCellStyle(style);
target.setCellValue(defaultValue(value, ""));
}
private CellStyle style(Sheet sheet, String reference) {
CellReference cellReference = new CellReference(reference);
return cell(row(sheet, cellReference.getRow()), cellReference.getCol()).getCellStyle();
}
private Row row(Sheet sheet, int rowIndex) {
Row row = sheet.getRow(rowIndex);
return row == null ? sheet.createRow(rowIndex) : row;
}
private Cell cell(Row row, int column) {
Cell cell = row.getCell(column);
return cell == null ? row.createCell(column) : cell;
}
private String value(Object value) {
return value == null ? "" : String.valueOf(value);
}
private String defaultValue(String value, String fallback) {
return isBlank(value) ? (fallback == null ? "" : fallback) : value;
}
private boolean isBlank(String value) {
return value == null || value.isBlank();
}
private record SchemaField(String path, String type, String description, String length,
boolean required, String example, boolean coded) {
}
}

View File

@@ -0,0 +1,4 @@
package io.shinhanlife.dap.mcg.document;
public record GeneratedDocument(String fileName, byte[] content) {
}

View File

@@ -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.mcc.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -1,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.mcc.dto.ToolMetadata;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.messaging; package io.shinhanlife.dap.mcg.messaging;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -10,16 +10,16 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.messaging * @package io.shinhanlife.dap.mcg.messaging
* @className KafkaProducerService * @className KafkaProducerService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -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.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.ChatClient;
@@ -11,6 +27,7 @@ import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@@ -26,6 +43,7 @@ public class ChatController {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final RedisRegistryService registryService; private final RedisRegistryService registryService;
private final ChatClient.Builder chatClientBuilder; private final ChatClient.Builder chatClientBuilder;
private final McpRouterController mcpRouterController;
@PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) @PostMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chatStream(@RequestBody Map<String, String> request, public SseEmitter chatStream(@RequestBody Map<String, String> request,
@@ -39,19 +57,45 @@ public class ChatController {
try { try {
List<ToolCallback> callbacks = new ArrayList<>(); List<ToolCallback> callbacks = new ArrayList<>();
for (ToolMetadata meta : registryService.getAllTools()) { JsonRpcResponse toolsResponse = mcpRouterController.listTools(null).getBody();
if (Boolean.TRUE.equals(meta.getVisible())) { if (toolsResponse != null && toolsResponse.getResult() instanceof Map) {
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId)); 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", "cohere/north-mini-code:free").trim();
if (selectedModel.isEmpty()) {
selectedModel = "cohere/north-mini-code:free";
}
// 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.") .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(); .build();
Flux<String> responseStream = chatClient.prompt() Flux<String> responseStream = activeChatClient.prompt()
.user(message) .user(message)
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools() .toolCallbacks(callbacks.toArray(new ToolCallback[0]))
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(selectedModel).build())
.stream() .stream()
.content(); .content();
@@ -74,7 +118,7 @@ public class ChatController {
} else if (errorMsg != null && errorMsg.contains("503")) { } else if (errorMsg != null && errorMsg.contains("503")) {
emitter.send("\n\n⚠ **AI 모델 서버 혼잡 (503)**: 현재 AI 모델을 제공하는 서버에 일시적으로 접속자가 많아 지연이 발생하고 있습니다. 잠시 후 다시 시도해 주세요."); emitter.send("\n\n⚠ **AI 모델 서버 혼잡 (503)**: 현재 AI 모델을 제공하는 서버에 일시적으로 접속자가 많아 지연이 발생하고 있습니다. 잠시 후 다시 시도해 주세요.");
} else if (errorMsg != null && errorMsg.contains("429")) { } 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 { } else {
emitter.send("\n[에러 발생: " + errorMsg + "]"); emitter.send("\n[에러 발생: " + errorMsg + "]");
} }

View File

@@ -0,0 +1,72 @@
package io.shinhanlife.dap.mcg.presentation;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClientResponseException;
/**
* Gateway 화면에서 들어온 DTO 엑셀 요청을 실제 DTO 클래스가 있는 Tool Pod로 전달한다.
* 브라우저가 동일 출처(8081)만 호출하도록 하여 CORS와 배포 주소 차이를 숨긴다.
*/
@RestController
public class DtoDownloadProxyController {
private static final MediaType XLSX_MEDIA_TYPE = MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
private final RestClient restClient;
private final String toolPodUrl;
public DtoDownloadProxyController(
RestClient.Builder restClientBuilder,
@Value("${mcp.gateway.fallback.default-url:http://localhost:8084}") String toolPodUrl) {
this.restClient = restClientBuilder.build();
this.toolPodUrl = toolPodUrl.replaceAll("/+$", "");
}
@GetMapping("/dto-download/options")
public List<String> options() {
// 셀렉트박스에 표시할 DTO 이름 목록을 Tool Pod에서 조회한다.
List<String> options = restClient.get()
.uri(toolPodUrl + "/dto-download/options")
.retrieve()
.body(new ParameterizedTypeReference<>() {});
return options == null ? List.of() : options;
}
@GetMapping("/dto-download/{dtoName}")
public ResponseEntity<byte[]> download(@PathVariable String dtoName) {
byte[] workbook;
try {
// 생성된 엑셀 바이트를 그대로 브라우저에 전달한다.
workbook = restClient.get()
.uri(toolPodUrl + "/dto-download/{dtoName}", dtoName)
.retrieve()
.body(byte[].class);
} catch (RestClientResponseException error) {
// 형식 불일치 등의 상태 코드와 오류 메시지도 변경 없이 전달한다.
return ResponseEntity.status(error.getStatusCode())
.contentType(error.getResponseHeaders() != null
&& error.getResponseHeaders().getContentType() != null
? error.getResponseHeaders().getContentType() : MediaType.TEXT_PLAIN)
.body(error.getResponseBodyAsByteArray());
}
String fileName = dtoName + ".xlsx";
return ResponseEntity.ok()
.contentType(XLSX_MEDIA_TYPE)
.contentLength(workbook == null ? 0 : workbook.length)
.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.attachment().filename(fileName).build().toString())
.body(workbook == null ? new byte[0] : workbook);
}
}

View File

@@ -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 lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -8,16 +8,16 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.presentation * @package io.shinhanlife.dap.mcg.presentation
* @className KillSwitchController * @className KillSwitchController
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -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 com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcRequest; import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.biz.mcp.adapter.dto.JsonRpcResponse; import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.biz.mcp.adapter.dto.Params; import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.service.ExecuteService; import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dap.common.mcp.security.SecurityProperties; import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -76,7 +91,7 @@ public class McpRouterController {
try { try {
Object result = executeService.execute(payload, effectiveTenantId); Object result = executeService.execute(payload, effectiveTenantId);
io.shinhanlife.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.setJsonrpc("2.0");
response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString()); response.setId(payload.containsKey("id") ? String.valueOf(payload.get("id")) : UUID.randomUUID().toString());
response.setResult(result); response.setResult(result);
@@ -97,13 +112,16 @@ public class McpRouterController {
} }
} }
@GetMapping("/tools/list") private List<ToolMetadata> fetchAllActiveTools() {
public ResponseEntity<JsonRpcResponse> listTools( List<ToolMetadata> activeTools = new ArrayList<>();
@RequestParam(value = "categoryKey", required = false) String categoryKey) { try {
List<ToolMetadata> activeTools = redisRegistryService.getAllTools() activeTools.addAll(redisRegistryService.getAllTools()
.stream() .stream()
.filter(ToolMetadata::getVisible) .filter(ToolMetadata::getVisible)
.collect(Collectors.toList()); .collect(Collectors.toList()));
} catch (org.springframework.data.redis.RedisConnectionFailureException exception) {
log.warn("Redis is unavailable. Fetching tools from configured fallback Tool Pods instead.");
}
Set<String> knownTools = activeTools.stream() Set<String> knownTools = activeTools.stream()
.map(ToolMetadata::getUid) .map(ToolMetadata::getUid)
@@ -133,6 +151,14 @@ public class McpRouterController {
log.warn("Failed to fetch local tools from fallback URL: {}", url); 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()) { if (categoryKey != null && !categoryKey.trim().isEmpty()) {
activeTools = activeTools.stream() activeTools = activeTools.stream()
@@ -146,12 +172,10 @@ public class McpRouterController {
return ResponseEntity.ok(response); return ResponseEntity.ok(response);
} }
@GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8") @GetMapping(value = "/tools/docs/markdown", produces = "text/markdown;charset=UTF-8")
public ResponseEntity<String> generateToolsMarkdown() { public ResponseEntity<String> generateToolsMarkdown() {
List<ToolMetadata> tools = redisRegistryService.getAllTools() List<ToolMetadata> tools = fetchAllActiveTools();
.stream()
.filter(ToolMetadata::getVisible)
.collect(Collectors.toList());
StringBuilder md = new StringBuilder(); StringBuilder md = new StringBuilder();
md.append("# \uD83E\uDD16 Shinhan AI Tool Catalog\n\n"); md.append("# \uD83E\uDD16 Shinhan AI Tool Catalog\n\n");

View File

@@ -0,0 +1,417 @@
package io.shinhanlife.dap.mcg.presentation;
/**
* @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 com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/scaffold")
public class ScaffoldingController {
private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of(
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "Enum", "List");
private static final Set<String> SUPPORTED_AI_MODELS = Set.of(
"inclusionai/ling-3.0-flash:free",
"openai/gpt-oss-20b:free",
"google/gemma-4-31b-it:free",
"nvidia/nemotron-3-nano-30b-a3b:free",
"cohere/north-mini-code:free");
private final ChatClient.Builder chatClientBuilder;
private final ObjectMapper objectMapper;
public ScaffoldingController(ChatClient.Builder chatClientBuilder, ObjectMapper objectMapper) {
this.chatClientBuilder = chatClientBuilder;
this.objectMapper = objectMapper;
}
@PostMapping("/pod")
public String scaffoldPod(@RequestBody Map<String, String> req) {
try {
String moduleName = req.getOrDefault("moduleName", "dap-was-oth");
if (!moduleName.startsWith("dap-was-")) moduleName = "dap-was-" + moduleName;
String port = req.getOrDefault("port", "8085");
String shortName = moduleName.replace("dap-was-", "").replace("-", "");
String author = req.get("author");
if (author == null || author.trim().isEmpty()) author = System.getProperty("user.name");
String date = req.get("date");
if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
return PodScaffolder.scaffoldPod(moduleName, port, shortName, author, date);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool")
public String scaffoldTool(@RequestBody Map<String, String> req) {
try {
String baseName = req.get("baseName");
String interfaceId = req.get("interfaceId");
String title = req.get("title");
if (title == null || title.isBlank()) title = baseName;
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-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");
String httpApiName = req.get("httpApiName");
String inputSchemaResource = req.get("inputSchemaResource");
String outputSchemaResource = req.get("outputSchemaResource");
List<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields"));
List<ToolScaffolder.FieldDefinition> outputFields = parseFields(req.get("outputFields"));
if (inputFields.isEmpty()) {
inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false));
}
ToolScaffolder.ToolDefinitionOptions definitionOptions = new ToolScaffolder.ToolDefinitionOptions(
req.get("functionDescription"),
req.get("whenToUse"),
req.get("whenNotToUse"),
req.get("ioLimits"),
req.get("displayDescription"),
parseDelimited(req.get("exampleQueries")),
parseDelimited(req.get("tags")),
req.get("ownerOrg"));
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType,
moduleName, author, date, register, clientSystemCode, inputSchemaResource,
outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions);
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@PostMapping("/tool-group")
public String scaffoldToolGroup(@RequestBody ToolGroupRequest request) {
try {
if (request == null || request.useCaseName() == null
|| !request.useCaseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("UseCase name must be PascalCase.");
}
String moduleName = request.moduleName() == null || request.moduleName().isBlank()
? "dap-was-oth" : request.moduleName().trim();
String author = request.author() == null || request.author().isBlank()
? System.getProperty("user.name") : request.author().trim();
String date = request.date() == null || request.date().isBlank()
? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim();
return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date,
request.tools() == null ? List.of() : request.tools());
} catch (Exception e) {
return "Error: " + safeMessage(e);
}
}
@GetMapping("/usecases")
public List<String> listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module.");
}
if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("Invalid domain category.");
}
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/"
+ categoryKey + "/usecase");
File[] files = useCaseDir.listFiles(file -> file.isFile() && file.getName().endsWith("UseCase.java"));
if (files == null) return List.of();
return Arrays.stream(files).map(File::getName)
.map(name -> name.substring(0, name.length() - ".java".length()))
.sorted().toList();
}
@PostMapping("/field-draft")
public ResponseEntity<?> generateFieldDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
String target = req.getOrDefault("target", "inputFields");
if (description.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "설명을 입력해주세요."));
}
if (!"inputFields".equals(target) && !"outputFields".equals(target)) {
return ResponseEntity.badRequest().body(Map.of("error", "지원하지 않는 필드 대상입니다."));
}
try {
String targetLabel = "inputFields".equals(target) ? "INPUT request DTO" : "OUTPUT response DTO";
String prompt = """
Generate Java DTO fields for an MCP tool.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List.
Enum fields must include enumValues. List fields must include itemType; use Object plus itemFields for object lists.
Generate fields only for the requested target: %s.
For OUTPUT fields, include resultCode and resultMessage when appropriate.
Keep field names valid Java camelCase identifiers. Generate at most 10 fields.
User description: %s
""".formatted(targetLabel, description);
String response = generateAiContent(prompt, req.get("model"));
FieldDraft draft = objectMapper.readValue(stripCodeFence(response), FieldDraft.class);
List<ToolScaffolder.FieldDefinition> fields = validateFields(draft.fields());
return ResponseEntity.ok(Map.of("fields", fields));
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI 초안 생성 실패: " + safeMessage(e)));
}
}
@PostMapping("/tool-draft")
public ResponseEntity<?> generateToolDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
if (description.isBlank()) {
return ResponseEntity.badRequest().body(Map.of("error", "Tool 설명을 입력해주세요."));
}
try {
String prompt = """
Generate an MCP Tool scaffold from the user request.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
categoryKey must be exactly three lowercase letters or digits.
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List. Enum must include enumValues; List must include itemType and object lists include itemFields.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
User request: %s
""".formatted(description);
String response = generateAiContent(prompt, req.get("model"));
ToolDraft draft = objectMapper.readValue(stripCodeFence(response), ToolDraft.class);
ToolDraft validatedDraft = validateToolDraft(draft);
return ResponseEntity.ok(validatedDraft);
} catch (Exception e) {
return ResponseEntity.internalServerError().body(Map.of("error", "AI Tool 초안 생성 실패: " + safeMessage(e)));
}
}
@PostMapping("/tool/update")
public String updateTool(@RequestBody Map<String, String> req) {
try {
String toolName = req.get("toolName");
String domainGroup = req.getOrDefault("categoryKey", req.get("domainGroup"));
String description = req.get("description");
boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true"));
Boolean requiresApproval = req.containsKey("requiresApproval") ? Boolean.parseBoolean(req.get("requiresApproval")) : null;
ToolSourceUpdater.updateToolSource(toolName, domainGroup, description, register, requiresApproval);
return "성공";
} catch (Exception e) {
return "오류 발생: " + e.getMessage();
}
}
@GetMapping("/modules")
public List<String> listModules() {
try {
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir);
File[] files = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith("dap-was-") && !f.getName().equals("dap-was-lib"));
if (files == null || files.length == 0) {
return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms");
}
return Arrays.stream(files).map(File::getName).sorted().collect(Collectors.toList());
} catch (Exception e) {
return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms");
}
}
private List<ToolScaffolder.FieldDefinition> parseFields(String source) throws Exception {
if (source == null || source.isBlank()) {
return List.of();
}
return objectMapper.readValue(source, new TypeReference<List<ToolScaffolder.FieldDefinition>>() { });
}
private List<String> parseDelimited(String source) {
if (source == null || source.isBlank()) {
return List.of();
}
return Arrays.stream(source.split("[\\r\\n,]+"))
.map(String::trim)
.filter(value -> !value.isBlank())
.distinct()
.toList();
}
private List<ToolScaffolder.FieldDefinition> validateFields(List<ToolScaffolder.FieldDefinition> source) {
if (source == null || source.isEmpty()) {
throw new IllegalArgumentException("AI가 필드를 생성하지 않았습니다.");
}
Set<String> names = new LinkedHashSet<>();
return source.stream()
.limit(10)
.filter(field -> field != null && field.name() != null && !field.name().isBlank())
.map(field -> new ToolScaffolder.FieldDefinition(
field.name().trim(),
field.type() == null ? "String" : field.type().trim(),
field.description() == null ? "" : field.description().trim(),
field.example() == null ? "" : field.example().trim(),
field.required(),
field.enumValues() == null ? List.of() : field.enumValues(),
field.itemType(),
field.itemFields() == null ? List.of() : field.itemFields()))
.peek(field -> {
if (!field.name().matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 필드명을 생성했습니다: " + field.name());
}
if (!SUPPORTED_FIELD_TYPES.contains(field.type())) {
throw new IllegalArgumentException("AI가 지원하지 않는 Type을 생성했습니다: " + field.type());
}
validateStructuredField(field);
if (!names.add(field.name())) {
throw new IllegalArgumentException("AI가 중복 필드명을 생성했습니다: " + field.name());
}
})
.toList();
}
private void validateStructuredField(ToolScaffolder.FieldDefinition field) {
if ("Enum".equals(field.type()) && field.enumValues().isEmpty()) {
throw new IllegalArgumentException("Enum field needs enumValues: " + field.name());
}
if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) {
throw new IllegalArgumentException("List field needs itemType: " + field.name());
}
if ("List".equals(field.type()) && "Object".equals(field.itemType()) && field.itemFields().isEmpty()) {
throw new IllegalArgumentException("Object List field needs itemFields: " + field.name());
}
}
private ToolDraft validateToolDraft(ToolDraft draft) {
if (draft == null || draft.baseName() == null || !draft.baseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Base Name을 생성했습니다.");
}
String categoryKey = draft.categoryKey() == null ? "" : draft.categoryKey().trim().toLowerCase(Locale.ROOT);
if (!categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Category Key를 생성했습니다.");
}
String routingType = draft.routingType() == null ? "" : draft.routingType().trim().toUpperCase(Locale.ROOT);
if (!Set.of("HTTP", "MCI").contains(routingType)) {
throw new IllegalArgumentException("AI가 지원하지 않는 Protocol을 생성했습니다.");
}
String httpApiName = draft.httpApiName() == null ? "http-api" : draft.httpApiName().trim();
if (!httpApiName.matches("^[A-Za-z0-9_-]+$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 HTTP API Name을 생성했습니다.");
}
String title = draft.title() == null ? "" : draft.title().trim();
String description = draft.description() == null ? "" : draft.description().trim();
if (title.isBlank() || description.isBlank()) {
throw new IllegalArgumentException("AI가 Tool 제목 또는 설명을 생성하지 않았습니다.");
}
String functionDescription = textOrDefault(draft.functionDescription(), description);
String displayDescription = textOrDefault(draft.displayDescription(), title);
String whenToUse = textOrDefault(draft.whenToUse(), description + " 요청을 처리할 때 사용한다.");
String whenNotToUse = textOrDefault(draft.whenNotToUse(), "필수 입력값이 없거나 다른 업무 요청에는 사용하지 않는다.");
String ioLimits = textOrDefault(draft.ioLimits(), "정의된 입력 필드만 허용하며 정의된 응답 DTO 범위만 반환한다.");
List<String> exampleQueries = normalizedDraftList(draft.exampleQueries(), List.of(
title + " 해줘", title + " 정보를 알려줘", title + " 결과를 확인해줘"));
List<String> tags = normalizedDraftList(draft.tags(), List.of(categoryKey));
String ownerOrg = textOrDefault(draft.ownerOrg(), "MCP_TOOL");
return new ToolDraft(draft.baseName().trim(), title, description, categoryKey, routingType, httpApiName,
functionDescription, displayDescription, whenToUse, whenNotToUse, ioLimits,
exampleQueries, tags, ownerOrg,
validateFields(draft.inputFields()), validateFields(draft.outputFields()));
}
private String textOrDefault(String value, String fallback) {
return value == null || value.isBlank() ? fallback : value.trim();
}
private List<String> normalizedDraftList(List<String> values, List<String> fallback) {
if (values == null) {
return fallback;
}
List<String> normalized = values.stream()
.filter(value -> value != null && !value.isBlank())
.map(String::trim)
.distinct()
.toList();
return normalized.isEmpty() ? fallback : normalized;
}
private String generateAiContent(String prompt, String requestedModel) {
return chatClientBuilder.build().prompt()
.user(prompt)
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(resolveModel(requestedModel)).build())
.call()
.content();
}
private String resolveModel(String requestedModel) {
String model = requestedModel == null || requestedModel.isBlank()
? "cohere/north-mini-code:free" : requestedModel.trim();
if (!SUPPORTED_AI_MODELS.contains(model)) {
throw new IllegalArgumentException("지원하지 않는 AI 모델입니다.");
}
return model;
}
private String stripCodeFence(String response) {
if (response == null) {
throw new IllegalArgumentException("AI 응답이 비어 있습니다.");
}
return response.trim().replaceFirst("^```(?:json)?\\s*", "").replaceFirst("\\s*```$", "").trim();
}
private String safeMessage(Exception e) {
return e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
}
private record FieldDraft(List<ToolScaffolder.FieldDefinition> fields) {
}
private record ToolDraft(String baseName, String title, String description, String categoryKey, String routingType,
String httpApiName, String functionDescription, String displayDescription,
String whenToUse, String whenNotToUse, String ioLimits,
List<String> exampleQueries, List<String> tags, String ownerOrg,
List<ToolScaffolder.FieldDefinition> inputFields,
List<ToolScaffolder.FieldDefinition> outputFields) {
}
private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date,
List<ToolScaffolder.ToolMethodDefinition> tools) {
}
}

View File

@@ -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.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

View File

@@ -1,9 +1,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; * @package io.shinhanlife.dap.mcg.redis
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata; * @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.mcc.dto.ToolMetadata;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
@@ -34,19 +48,16 @@ public class RedisToolTraceService {
private final ObjectProvider<StringRedisTemplate> redisProvider; private final ObjectProvider<StringRedisTemplate> redisProvider;
private final ObjectMapper json; private final ObjectMapper json;
private final McpMonitorEventService monitorEvents; private final McpMonitorEventService monitorEvents;
private final SensitiveDataMasker masker;
private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>(); private final Map<String, AttemptState> attemptStates = new ConcurrentHashMap<>();
public RedisToolTraceService(McpGatewayProperties properties, public RedisToolTraceService(McpGatewayProperties properties,
ObjectProvider<StringRedisTemplate> redisProvider, ObjectProvider<StringRedisTemplate> redisProvider,
ObjectMapper json, ObjectMapper json,
McpMonitorEventService monitorEvents, McpMonitorEventService monitorEvents) {
SensitiveDataMasker masker) {
this.properties = properties; this.properties = properties;
this.redisProvider = redisProvider; this.redisProvider = redisProvider;
this.json = json; this.json = json;
this.monitorEvents = monitorEvents; this.monitorEvents = monitorEvents;
this.masker = masker;
} }
/** /**
@@ -191,7 +202,7 @@ public class RedisToolTraceService {
arguments.fieldNames().forEachRemaining(argNames::add); arguments.fieldNames().forEachRemaining(argNames::add);
trace.put("argumentNames", argNames); trace.put("argumentNames", argNames);
trace.put("arguments", masker.mask(arguments).toString()); trace.put("arguments", arguments.toString());
trace.put("responseSummary", responseSummary(responseText)); trace.put("responseSummary", responseSummary(responseText));
trace.put("timestamp", Instant.now().toString()); trace.put("timestamp", Instant.now().toString());
return json.writeValueAsString(trace); return json.writeValueAsString(trace);

View File

@@ -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.mcc.dto.ToolMetadata;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -13,16 +13,16 @@ import java.util.stream.Collectors;
import java.util.Objects; import java.util.Objects;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.registry * @package io.shinhanlife.dap.mcg.registry
* @className RedisRegistryService * @className RedisRegistryService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -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.Clock;
import java.time.Instant; import java.time.Instant;

View File

@@ -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.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -1,6 +1,21 @@
package io.shinhanlife.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 org.springframework.stereotype.Service;
import java.time.Clock; import java.time.Clock;

View File

@@ -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 상태 정보입니다. * MCP Monitor 화면에 내려주는 Circuit Breaker 상태 정보입니다.
*/ */

View File

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

View File

@@ -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 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다. * Tool/EIMS/Tool 서버 호출 실패를 업무적으로 구분하기 위한 장애 유형입니다.
* *

View File

@@ -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 호출 재시도 정책입니다. * Tool 호출 재시도 정책입니다.
* *

View File

@@ -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과 함께 전달하는 공통 예외입니다. * Tool 실행 발생한 오류를 FailureType과 함께 전달하는 공통 예외입니다.
* *

View File

@@ -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; import java.util.Set;
/** /**

View File

@@ -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 jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;

View File

@@ -1,10 +1,25 @@
package io.shinhanlife.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; * @package io.shinhanlife.dap.mcg.security
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType; * @className ToolAuthorizationService
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata; * @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.mcc.dto.ToolMetadata;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Set; import java.util.Set;

View File

@@ -1,20 +1,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; * @package io.shinhanlife.dap.mcg.service
import io.shinhanlife.dap.biz.mcp.gateway.resilience.ToolExecutionException; * @className ExecuteService
import io.shinhanlife.dap.biz.mcp.gateway.dto.OperationType; * @description AX HUB 시스템 처리 클래스
import io.shinhanlife.dap.biz.mcp.gateway.config.McpGatewayProperties; * @author 0986406
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.SensitiveDataMasker; * @create 2026.09.01
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.GuardrailService; * <pre>
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; * 2026.09.01 0986406 최초생성
import io.shinhanlife.dap.biz.mcp.gateway.resilience.CircuitBreakerService; *
import io.shinhanlife.dap.biz.mcp.gateway.security.ToolAuthorizationService; * </pre>
import io.shinhanlife.dap.biz.mcp.gateway.redis.RedisToolTraceService; */
import java.util.HashMap;
import io.shinhanlife.dap.mcc.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 jakarta.annotation.PreDestroy;
import java.util.Map; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.LargeToolResponseService; import io.shinhanlife.dap.mcg.tool.large.LargeToolResponseService;
import io.shinhanlife.dap.biz.mcp.gateway.tool.large.PaginationRequestValidator; import io.shinhanlife.dap.mcg.tool.large.PaginationRequestValidator;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResultFormatter; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResultFormatter;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import io.shinhanlife.dap.biz.mcp.gateway.guardrail.ToolResponseGuardrailService; import io.shinhanlife.dap.mcg.guardrail.ToolResponseGuardrailService;
import io.shinhanlife.dap.biz.mcp.gateway.transport.ToolInvoker; import io.shinhanlife.dap.mcg.transport.ToolInvoker;
@Slf4j @Slf4j
@Service @Service
@@ -41,7 +57,6 @@ public class ExecuteService {
private final ToolPlanner planner; private final ToolPlanner planner;
private final KillSwitchService killSwitchService; private final KillSwitchService killSwitchService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final SensitiveDataMasker dataMasker;
private final GuardrailService guardrailService; private final GuardrailService guardrailService;
private final McpRequestContextResolver contextResolver; private final McpRequestContextResolver contextResolver;
private final AuditLogService auditLogService; private final AuditLogService auditLogService;
@@ -59,7 +74,6 @@ public class ExecuteService {
public ExecuteService(ToolPlanner planner, public ExecuteService(ToolPlanner planner,
KillSwitchService killSwitchService, KillSwitchService killSwitchService,
ObjectMapper objectMapper, ObjectMapper objectMapper,
SensitiveDataMasker dataMasker,
GuardrailService guardrailService, GuardrailService guardrailService,
McpRequestContextResolver contextResolver, McpRequestContextResolver contextResolver,
AuditLogService auditLogService, AuditLogService auditLogService,
@@ -75,7 +89,6 @@ public class ExecuteService {
this.planner = planner; this.planner = planner;
this.killSwitchService = killSwitchService; this.killSwitchService = killSwitchService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.dataMasker = dataMasker;
this.guardrailService = guardrailService; this.guardrailService = guardrailService;
this.contextResolver = contextResolver; this.contextResolver = contextResolver;
this.auditLogService = auditLogService; this.auditLogService = auditLogService;
@@ -145,22 +158,53 @@ public class ExecuteService {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
String responseText = ""; String responseText = "";
try { responseText = objectMapper.writeValueAsString(result); } catch (Exception ignore) {} long originalSize = 0;
try {
responseText = objectMapper.writeValueAsString(result);
originalSize = responseText.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
} catch (Exception ignore) {}
Map<String, Object> finalResult = new java.util.LinkedHashMap<>();
finalResult.put("status", "ok");
finalResult.put("result", result);
finalResult.put("error_code", null);
finalResult.put("error_message", null);
finalResult.put("elapsed_ms", elapsedMillis);
finalResult.put("truncated", false);
finalResult.put("original_size", originalSize);
auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, ""); auditLogService.toolFinished(context, metadata.getName(), elapsedMillis, true, "");
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, true, "", responseText);
return result; return finalResult;
} catch (ToolExecutionException error) { } catch (ToolExecutionException error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, error.failureType().name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), ""); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, error.failureType().name(), "");
throw error;
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", error.failureType().name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
} catch (Exception error) { } catch (Exception error) {
long elapsedMillis = elapsedMillis(startedAt); long elapsedMillis = elapsedMillis(startedAt);
auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name()); auditLogService.toolFinished(context, toolName, elapsedMillis, false, FailureType.INTERNAL_ERROR.name());
redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), ""); redisTrace.finished(context, metadata, argumentsNode, elapsedMillis, false, FailureType.INTERNAL_ERROR.name(), "");
throw new ToolExecutionException(FailureType.INTERNAL_ERROR, "Tool execution failed: " + toolName, error);
Map<String, Object> errorResult = new java.util.LinkedHashMap<>();
errorResult.put("status", "error");
errorResult.put("result", null);
errorResult.put("error_code", FailureType.INTERNAL_ERROR.name());
errorResult.put("error_message", error.getMessage());
errorResult.put("elapsed_ms", elapsedMillis);
errorResult.put("truncated", false);
errorResult.put("original_size", 0);
return errorResult;
} }
} }
@@ -213,34 +257,33 @@ public class ExecuteService {
if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) { if (metadata.getPodUrl() != null && !metadata.getPodUrl().isEmpty()) {
targetUrl = metadata.getPodUrl(); targetUrl = metadata.getPodUrl();
} }
String executeApiUrl = targetUrl + "/mcp/api/v1/tools/call";
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()); LargeToolResponseService.Collector collector = largeResponses.newCollector(metadata.getName(), context.requestId());
while (true) { while (true) {
Map<String, Object> pagePayload = new java.util.HashMap<>(payload); Map<String, Object> pagePayload = objectMapper.convertValue(pageArguments, Map.class);
if (pagePayload.containsKey("params")) {
Map<String, Object> params = new java.util.HashMap<>((Map<String, Object>) pagePayload.get("params"));
params.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
pagePayload.put("params", params);
} else {
pagePayload.put("arguments", objectMapper.convertValue(pageArguments, Map.class));
}
try { try {
log.info(" [ExecuteService] 요청 페이로드(마스킹 적용): {}", objectMapper.writeValueAsString(dataMasker.mask(objectMapper.valueToTree(pagePayload)))); log.info(" [ExecuteService] 요청 페이로드: {}", objectMapper.writeValueAsString(pagePayload));
} catch (Exception ignore) {} } catch (Exception ignore) {}
JsonNode data = null; JsonNode data = null;
try { 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) { } catch (Exception e) {
if (executeApiUrl.contains("http://tool-")) { if (targetUrl.contains("http://tool-")) {
String fallbackUrl = executeApiUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost"); String fallbackUrl = targetUrl.replaceAll("http://tool-[a-zA-Z0-9-]+", "http://localhost");
log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl); log.warn(" [ExecuteService] 호스트를 찾을 수 없어 localhost로 재시도합니다: {}", fallbackUrl);
try { try {
data = toolInvoker.invoke(pagePayload, fallbackUrl); data = toolInvoker.invoke(metadata.getName(), pagePayload, fallbackUrl, headers);
} catch (Exception ex) { } catch (Exception ex) {
throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage()); throw new ToolExecutionException(FailureType.SERVER_ERROR, "Tool Pod 호출 실패 (localhost 재시도 포함): " + ex.getMessage());
} }

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.biz.mcp.gateway.service; package io.shinhanlife.dap.mcg.service;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -6,16 +6,16 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.service * @package io.shinhanlife.dap.mcg.service
* @className KillSwitchService * @className KillSwitchService
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -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.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.config.GatewayFallbackProperties; import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -12,16 +12,16 @@ import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.service * @package io.shinhanlife.dap.mcg.service
* @className ToolPlanner * @className ToolPlanner
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */
@@ -81,7 +81,7 @@ public class ToolPlanner {
} }
// 2-1. [신규] 도메인 그룹핑 기반 권한 검증 // 2-1. [신규] 도메인 그룹핑 기반 권한 검증
if (tenantId != null && toolMetadata.getCategoryKey() != null) { if (tenantId != null && !tenantId.equalsIgnoreCase("system") && toolMetadata.getCategoryKey() != null) {
String normalizedTenantId = tenantId.toLowerCase(); String normalizedTenantId = tenantId.toLowerCase();
List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId); List<String> allowedDomains = securityProperties.getTenantDomains().get(normalizedTenantId);

View File

@@ -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 com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.spec.McpSchema; 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.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.servlet.function.RouterFunction; import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions; import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerResponse; import org.springframework.web.servlet.function.ServerResponse;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.Map; import java.util.Map;
@@ -28,6 +49,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
private final String sseEndpoint; private final String sseEndpoint;
private final String messageEndpoint; private final String messageEndpoint;
private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>(); private final Map<String, McpServerSession> sessions = new ConcurrentHashMap<>();
private final Map<String, CustomMcpSessionTransport> customTransports = new ConcurrentHashMap<>();
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public CustomWebMvcSseServerTransportProvider(String sseEndpoint, String messageEndpoint, ObjectMapper objectMapper) { public CustomWebMvcSseServerTransportProvider(String sseEndpoint, String messageEndpoint, ObjectMapper objectMapper) {
@@ -50,6 +72,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
} catch (Exception ignored) {} } catch (Exception ignored) {}
}); });
sessions.clear(); sessions.clear();
customTransports.clear();
}); });
} }
@@ -60,25 +83,25 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
.toList()); .toList());
} }
public org.springframework.web.servlet.mvc.method.annotation.SseEmitter handleSse() { public SseEmitter handleSse() {
if (sessionFactory == null) { if (sessionFactory == null) {
throw new IllegalStateException("SessionFactory not configured"); 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(); String sessionId = UUID.randomUUID().toString();
CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId); CustomMcpSessionTransport sessionTransport = new CustomMcpSessionTransport(emitter, sessionId);
McpServerSession session = sessionFactory.create(sessionTransport); McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session); sessions.put(sessionId, session);
emitter.onCompletion(() -> sessions.remove(sessionId)); // emitter.onCompletion(() -> sessions.remove(sessionId));
emitter.onTimeout(() -> sessions.remove(sessionId)); // emitter.onTimeout(() -> sessions.remove(sessionId));
new Thread(() -> { new Thread(() -> {
try { try {
Thread.sleep(100); 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) { } catch (Exception e) {
emitter.completeWithError(e); emitter.completeWithError(e);
} }
@@ -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); 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); boolean isNew = !sessions.containsKey(sessionId);
McpServerSession session = sessionFactory.create(sessionTransport);
sessions.put(sessionId, session);
emitter.onCompletion(() -> sessions.remove(sessionId)); if (isNew) {
emitter.onTimeout(() -> sessions.remove(sessionId)); 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(() -> { new Thread(() -> {
try { try {
// 커스텀 클라이언트는 endpoint 이벤트를 무시할 있지만, 표준 호환성을 위해 전송 // Body로 들어온 메시지 즉시 비동기 처리
Thread.sleep(100);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("endpoint").data(messageEndpoint + "?sessionId=" + sessionId));
// Body로 들어온 initialize 즉시 처리
if (body != null && !body.trim().isEmpty()) { if (body != null && !body.trim().isEmpty()) {
handleMessage(sessionId, body); handleMessage(sessionId, body, emitter);
} }
} catch (Exception e) { } catch (Exception e) {
emitter.completeWithError(e); emitter.completeWithError(e);
@@ -120,38 +150,52 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
} }
public org.springframework.http.ResponseEntity<String> handleMessage(String sessionId, String body) { 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); log.info("Received POST message for sessionId: " + sessionId + ", body: " + body);
if (sessionId == null || !sessions.containsKey(sessionId)) { 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); McpServerSession session = sessions.get(sessionId);
try { try {
java.util.Map<String, Object> map = objectMapper.readValue(body, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, Object>>() {}); Map<String, Object> map = objectMapper.readValue(body, new TypeReference<Map<String, Object>>() {});
io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage message; JSONRPCMessage message;
if (map.containsKey("id")) { if (map.containsKey("id")) {
if (map.containsKey("method")) { if (map.containsKey("method")) {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest.class); message = objectMapper.convertValue(map, JSONRPCRequest.class);
} else { } else {
message = objectMapper.convertValue(map, io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.class); message = objectMapper.convertValue(map, JSONRPCResponse.class);
} }
} else { } 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()); log.info("Converted message type: " + message.getClass().getName());
session.handle(message).subscribe(); session.handle(message).subscribe();
log.info("Message sent to session handler"); 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) { } catch (Exception e) {
log.error("Failed to handle message", 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 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; private final String sessionId;
public CustomMcpSessionTransport(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter, 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; this.sessionId = sessionId;
} }
public void setEmitter(org.springframework.web.servlet.mvc.method.annotation.SseEmitter emitter) {
this.emitter = emitter;
}
@Override @Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) { public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return Mono.fromRunnable(() -> { return Mono.fromRunnable(() -> {
@@ -166,18 +214,30 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
try { try {
String json = objectMapper.writeValueAsString(message); String json = objectMapper.writeValueAsString(message);
log.info("Serialized message: " + json); log.info("Serialized message: " + json);
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(json)); if (this.emitter != null) {
log.info("Message successfully sent to SSE emitter"); 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(); // MCP 표준 클라이언트 지원을 위해 스트림 강제 종료 제거
}
} catch (Exception e) { } catch (Exception e) {
log.error("Error sending message to SSE emitter", e); log.error("Error sending message to SSE emitter", e);
emitter.completeWithError(e); if (this.emitter != null) {
this.emitter.completeWithError(e);
}
} }
}); });
} }
@Override @Override
public Mono<Void> closeGracefully() { public Mono<Void> closeGracefully() {
return Mono.fromRunnable(emitter::complete); return Mono.fromRunnable(() -> {
if (this.emitter != null) {
this.emitter.complete();
}
});
} }
@Override @Override

View File

@@ -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.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; 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.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
@RestController @RestController
public class DynamicMcpController { public class DynamicMcpController {
@@ -19,24 +37,6 @@ public class DynamicMcpController {
this.manager = manager; 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}") @GetMapping("/mcp/sse/{category}")
public SseEmitter handleSse(@PathVariable("category") String category) { public SseEmitter handleSse(@PathVariable("category") String category) {
CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category); CustomWebMvcSseServerTransportProvider transport = manager.getTransport(category);
@@ -70,18 +70,33 @@ public class DynamicMcpController {
return ResponseEntity.badRequest().body("Unknown category: " + category); return ResponseEntity.badRequest().body("Unknown category: " + category);
} }
if (sessionId == null || sessionId.isEmpty()) { boolean isNew = (sessionId == null || sessionId.isEmpty());
// 세션 생성 (initialize 요청) String activeSessionId = isNew ? java.util.UUID.randomUUID().toString() : sessionId;
String newSessionId = java.util.UUID.randomUUID().toString();
SseEmitter emitter = transport.handleCustomSse(newSessionId, body);
return ResponseEntity.ok() if (!isNew && !transport.hasSession(activeSessionId)) {
.header("Mcp-Session-Id", newSessionId) // 클라이언트가 보낸 세션 ID가 만료되었거나 존재하지 않는 경우 (Warm Pool 스펙: 404 Not Found 반환)
.body(emitter); return ResponseEntity.notFound().build();
} else {
// 기존 세션 메시지 전송 (tools/call )
transport.handleMessage(sessionId, body);
return ResponseEntity.accepted().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());
} }
} }

View File

@@ -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.McpServer;
import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.biz.mcp.gateway.dto.ToolMetadata; import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -36,7 +53,9 @@ public class DynamicMcpServerManager {
getOrCreateServer("payment"); getOrCreateServer("payment");
getOrCreateServer("sms"); getOrCreateServer("sms");
getOrCreateServer("email"); getOrCreateServer("email");
getOrCreateServer("other"); getOrCreateServer("oth");
getOrCreateServer("sample");
getOrCreateServer("smp"); // smp 카테고리도 기본적으로 항상 열어두도록 추가
} }
private McpSyncServer getOrCreateServer(String categoryKey) { private McpSyncServer getOrCreateServer(String categoryKey) {
@@ -50,8 +69,8 @@ public class DynamicMcpServerManager {
CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper); CustomWebMvcSseServerTransportProvider transport = new CustomWebMvcSseServerTransportProvider(ssePath, msgPath, objectMapper);
McpSyncServer newServer = McpServer.sync(transport) McpSyncServer newServer = McpServer.sync(transport)
.serverInfo("AXHUB-Gateway-" + key, "1.0.0") .serverInfo("dap-was-" + key, "1.0.0")
.capabilities(io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.builder().tools(true).build()) .capabilities(ServerCapabilities.builder().tools(true).build())
.build(); .build();
categoryTransports.put(key, transport); categoryTransports.put(key, transport);

View File

@@ -1,10 +1,26 @@
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.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.mcp.ToolMetadataMcpMapper;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.biz.mcp.gateway.tool.result.ToolExecutionResult; import io.shinhanlife.dap.mcg.tool.result.ToolExecutionResult;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -33,11 +49,7 @@ public class RegistryMcpToolSpecificationFactory {
* Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다. * Registry Entry 하나를 MCP SDK의 stateless sync Tool specification으로 변환합니다.
*/ */
public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) { public McpServerFeatures.SyncToolSpecification create(ToolMetadata entry) {
McpSchema.Tool tool = McpSchema.Tool.builder() McpSchema.Tool tool = ToolMetadataMcpMapper.toTool(entry);
.name(entry.getName())
.description(description(entry))
.inputSchema(inputSchema(entry))
.build();
return McpServerFeatures.SyncToolSpecification.builder() return McpServerFeatures.SyncToolSpecification.builder()
.tool(tool) .tool(tool)
@@ -126,6 +138,20 @@ public class RegistryMcpToolSpecificationFactory {
return schema; return schema;
} }
@SuppressWarnings("unchecked")
private McpSchema.JsonSchema toJsonSchema(Map<String, Object> schema) {
return new McpSchema.JsonSchema(
String.valueOf(schema.getOrDefault("type", "object")),
schema.get("properties") instanceof Map<?, ?> properties
? (Map<String, Object>) properties : Map.of(),
schema.get("required") instanceof List<?> required
? (List<String>) required : List.of(),
schema.get("additionalProperties") instanceof Boolean additionalProperties
? additionalProperties : Boolean.TRUE,
schema.get("$defs") instanceof Map<?, ?> defs ? (Map<String, Object>) defs : Map.of(),
schema.get("definitions") instanceof Map<?, ?> definitions
? (Map<String, Object>) definitions : Map.of());
}
private String description(ToolMetadata entry) { private String description(ToolMetadata entry) {
return entry.getDescription() == null || entry.getDescription().isBlank() return entry.getDescription() == null || entry.getDescription().isBlank()
? entry.getName() + " Tool" ? entry.getName() + " Tool"

View File

@@ -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.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.biz.mcp.gateway.registry.RedisRegistryService; import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -19,16 +19,16 @@ import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
* @package io.shinhanlife.dap.biz.mcp.gateway.sync * @package io.shinhanlife.dap.mcg.sync
* @className RegistryMcpToolSynchronizer * @className RegistryMcpToolSynchronizer
* @description AX HUB 시스템 처리 클래스 * @description AX HUB 시스템 처리 클래스
* @author 김형식 * @author 0986406
* @create 2026.09.01 * @create 2026.09.01
* <pre> * <pre>
* ---------- 개정이력 ---------- * ---------- 개정이력 ----------
* 수정일 수정자 수정내용 * 수정일 수정자 수정내용
* ---------- -------- --------------------------- * ---------- -------- ---------------------------
* 2026.09.01 김형식 최초생성 * 2026.09.01 0986406 최초생성
* *
* </pre> * </pre>
*/ */

View File

@@ -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 org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -12,12 +29,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
public class AgentResponseBudgetService { public class AgentResponseBudgetService {
private final AgentResponseBudgetProperties properties; private final AgentResponseBudgetProperties properties;
private final ObjectMapper json; private final ObjectMapper json;
private final SensitiveDataMasker masker; public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json) {
public AgentResponseBudgetService(AgentResponseBudgetProperties properties, ObjectMapper json, SensitiveDataMasker masker) {
this.properties = properties; this.properties = properties;
this.json = json; this.json = json;
this.masker = masker;
} }
public ObjectNode apply(ObjectNode response) { public ObjectNode apply(ObjectNode response) {
@@ -91,7 +105,7 @@ public class AgentResponseBudgetService {
} }
private JsonNode budgetItem(JsonNode item, BudgetStats stats) { private JsonNode budgetItem(JsonNode item, BudgetStats stats) {
JsonNode masked = masker.mask(item); JsonNode masked = item;
if (!masked.isObject()) { if (!masked.isObject()) {
return truncateByBytes(masked, stats); return truncateByBytes(masked, stats);
} }
@@ -99,9 +113,9 @@ public class AgentResponseBudgetService {
int copied = 0; int copied = 0;
int originalFields = 0; int originalFields = 0;
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = masked.fields(); Iterator<Map.Entry<String, JsonNode>> fields = masked.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next(); Map.Entry<String, JsonNode> entry = fields.next();
originalFields++; originalFields++;
if (copied >= properties.maxFieldsPerItem()) { if (copied >= properties.maxFieldsPerItem()) {
continue; continue;
@@ -135,9 +149,9 @@ public class AgentResponseBudgetService {
ObjectNode source = (ObjectNode) item; ObjectNode source = (ObjectNode) item;
ObjectNode limited = json.createObjectNode(); ObjectNode limited = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields(); Iterator<Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next(); Map.Entry<String, JsonNode> entry = fields.next();
JsonNode value = entry.getValue(); JsonNode value = entry.getValue();
if (value.isTextual()) { if (value.isTextual()) {
limited.put(entry.getKey(), truncateText(value.asText())); limited.put(entry.getKey(), truncateText(value.asText()));
@@ -164,7 +178,7 @@ public class AgentResponseBudgetService {
fallback.put("maxItemBytes", properties.maxItemBytes()); fallback.put("maxItemBytes", properties.maxItemBytes());
if (item.isObject()) { if (item.isObject()) {
ArrayNode fieldNames = json.createArrayNode(); ArrayNode fieldNames = json.createArrayNode();
java.util.Iterator<String> fieldNamesIter = item.fieldNames(); Iterator<String> fieldNamesIter = item.fieldNames();
while (fieldNamesIter.hasNext()) { while (fieldNamesIter.hasNext()) {
fieldNames.add(fieldNamesIter.next()); fieldNames.add(fieldNamesIter.next());
} }
@@ -188,9 +202,9 @@ public class AgentResponseBudgetService {
private ObjectNode copyWithout(ObjectNode source, String... excludedFields) { private ObjectNode copyWithout(ObjectNode source, String... excludedFields) {
ObjectNode copy = json.createObjectNode(); ObjectNode copy = json.createObjectNode();
java.util.Iterator<java.util.Map.Entry<String, JsonNode>> fields = source.fields(); Iterator<Map.Entry<String, JsonNode>> fields = source.fields();
while (fields.hasNext()) { while (fields.hasNext()) {
java.util.Map.Entry<String, JsonNode> entry = fields.next(); Map.Entry<String, JsonNode> entry = fields.next();
if (!excluded(entry.getKey(), excludedFields)) { if (!excluded(entry.getKey(), excludedFields)) {
copy.set(entry.getKey(), entry.getValue()); copy.set(entry.getKey(), entry.getValue());
} }

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