refactor: restructure packages, delete ZtUsac, fix gateway context
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m15s

This commit is contained in:
jade
2026-08-05 10:09:36 +09:00
parent 65222b5b1a
commit 4b34df774d
52 changed files with 1370 additions and 519 deletions

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.mcg.config;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.guardrail;
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.stereotype.Service;

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.guardrail;
*/
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -16,7 +16,7 @@ package io.shinhanlife.dap.mcg.presentation;
* </pre>
*/
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import lombok.RequiredArgsConstructor;

View File

@@ -20,7 +20,7 @@ import io.shinhanlife.dap.lib.adapter.dto.JsonRpcRequest;
import io.shinhanlife.dap.lib.adapter.dto.JsonRpcResponse;
import io.shinhanlife.dap.lib.adapter.dto.Params;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.redis;
*/
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.security.McpRequestContext;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.mcg.registry;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;

View File

@@ -19,7 +19,7 @@ import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;
import io.shinhanlife.dap.lib.dto.OperationType;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import org.springframework.stereotype.Service;
import java.util.Set;

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.mcg.service;
*/
import java.util.HashMap;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
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;

View File

@@ -2,7 +2,7 @@ package io.shinhanlife.dap.mcg.service;
import io.shinhanlife.dap.lib.mcp.security.SecurityProperties;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.GatewayFallbackProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -18,7 +18,7 @@ package io.shinhanlife.dap.mcg.sync;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.CustomWebMvcSseServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -15,7 +15,7 @@ package io.shinhanlife.dap.mcg.sync;
*
* </pre>
*/
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.service.ExecuteService;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.spec.McpSchema;

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.mcg.sync;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.registry.RedisRegistryService;
import io.modelcontextprotocol.server.McpSyncServer;
import org.slf4j.Logger;

View File

@@ -15,7 +15,7 @@ package io.shinhanlife.dap.mcg.tool.large;
*
* </pre>
*/
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.config.McpGatewayProperties;
import io.shinhanlife.dap.mcg.resilience.FailureType;
import io.shinhanlife.dap.mcg.resilience.ToolExecutionException;

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.shinhanlife.axhub.common.session.domain.repository.ZtUsacRepository">
<select id="selectZtUsac" resultType="io.shinhanlife.axhub.common.session.dto.ZtUsacOutDto">
SELECT
AU.PRAF_NO, -- 인사번호
AU.PRAF_NM, -- 인사명
AU.OGNZ_NO, -- 조직번호
OG.OGNZ_NM, -- 조직명
AU.ADDRE, -- 이메일주소
AU.PRAF_OFDU_CD, -- 인사직무코드
AU.PRAF_OFDU_NM, -- 인사직무명
AU.PRAF_OFLE_CD, -- 인사직급코드
AU.PRAF_OFLE_NM, -- 인사직급명
AU.PRAF_DUTY_CD, -- 인사직책코드
AU.PRAF_DUTY_NM, -- 인사직책명
AU.MNGR_PRAF_NO, -- 관리자인사번호
AU.PUSE_YN, -- 사용여부
LISTAGG(URR.ROLE_NO, ',') WITHIN GROUP (ORDER BY URR.ROLE_NO) AS ROLE_NO_STR_LIST,
LISTAGG(UR.ROLE_NM, ',') WITHIN GROUP (ORDER BY URR.ROLE_NO) AS ROLE_NM_STR_LIST,
LISTAGG(AM.TGTR_PRAF_NO, ',') WITHIN GROUP (ORDER BY AM.TGTR_PRAF_NO) AS TGTR_PRAF_NO_STR_LIST,
LISTAGG(AM.TGTR_OGNZ_NO, ',') WITHIN GROUP (ORDER BY AM.TGTR_PRAF_NO) AS TGTR_OGNZ_NO_STR_LIST
FROM
S_EIAM_TIS.AA_USAC AU
INNER JOIN
S_EIAM_TIS.AA_USAC_USER_GROU_RTNS UGR ON AU.PRAF_NO = UGR.PRAF_NO
INNER JOIN
S_EIAM_TIS.AA_USER_GROU UG ON UGR.USER_GROU_ID = UG.USER_GROU_ID
INNER JOIN
S_EIAM_TIS.AA_USER_GROU_ROLE_RTNS URR ON UG.USER_GROU_ID = URR.USER_GROU_ID
INNER JOIN
S_EIAM_TIS.AA_ROLE UR ON URR.ROLE_NO = UR.ROLE_NO
LEFT OUTER JOIN
S_EIAM_TIS.AA_MNDT AM ON AM.PRAF_NO = AU.PRAF_NO
INNER JOIN
S_EIAM_TIS.AA_OGNZ OG ON OG.OGNZ_NO = AU.OGNZ_NO
WHERE
AU.PRAF_NO = #{prafNo}
GROUP BY
AU.PRAF_NO, AU.PRAF_NM, AU.OGNZ_NO, AU.ADDRE, AU.PRAF_OFDU_CD, AU.PRAF_OFDU_NM, AU.PRAF_OFLE_CD, AU.PRAF_OFLE_NM, AU.PRAF_DUTY_CD, AU.PRAF_DUTY_NM, AU.MNGR_PRAF_NO, AU.PUSE_YN;
</select>
</mapper>

View File

@@ -5,7 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcg.sync.RegistryMcpToolSpecificationFactory;
import org.junit.jupiter.api.Test;

View File

@@ -2,8 +2,8 @@ package io.shinhanlife.dap.lib.manifest;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.lib.usecase.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Comparator;

View File

@@ -1,12 +1,12 @@
package io.shinhanlife.dap.lib.usecase;
package io.shinhanlife.dap.lib.mcp;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.spec.McpSchema;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
import io.shinhanlife.dap.lib.usecase.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
import java.util.LinkedHashMap;
@@ -16,8 +16,11 @@ import org.springframework.context.event.EventListener;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
@Component
@ConditionalOnBean(BusinessToolController.class)
public class ToolPodMcpToolSynchronizer {
private final McpSyncServer mcpServer;
private final ToolRegistryHeartbeatSender heartbeatSender;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.lib.usecase;
package io.shinhanlife.dap.lib.mcp;
/**
@@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import jakarta.annotation.PostConstruct;
import java.lang.reflect.Method;
@@ -43,11 +43,15 @@ import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
@Slf4j
@Component
@Configuration
@EnableScheduling
@RequiredArgsConstructor
@ConditionalOnBean(BusinessToolController.class)
public class ToolRegistryHeartbeatSender {
private final ApplicationContext applicationContext;

View File

@@ -1,29 +0,0 @@
package io.shinhanlife.dap.lib.session.converter;
import io.shinhanlife.dap.lib.session.dto.SessionDto;
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @package io.shinhanlife.dap.lib.session.converter
* @className ZtUsacConverter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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.lib.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.lib.session.domain.model
* @className ZtUsacModel
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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.lib.session.domain.repository;
import io.shinhanlife.glow.GlowMybatisMapper;
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
/**
* @package io.shinhanlife.dap.lib.session.domain.repository
* @className ZtUsacRepository
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@GlowMybatisMapper
public interface ZtUsacRepository {
/**
* 사용자 조회 (단건)
*
* @param dto 사번
* @return 인사정보
*/
ZtUsacOutDto selectZtUsac(ZtUsacInDto dto);
}

View File

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

View File

@@ -1,42 +0,0 @@
package io.shinhanlife.dap.lib.session.domain.usecase.impl;
import io.shinhanlife.dap.lib.session.domain.repository.ZtUsacRepository;
import io.shinhanlife.dap.lib.session.domain.usecase.ZtUsacUseCase;
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @package io.shinhanlife.dap.lib.session.domain.usecase.impl
* @className ZtUsacUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Service
@RequiredArgsConstructor
public class ZtUsacUseCaseImpl implements ZtUsacUseCase {
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.lib.session.dto;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.lib.session.dto
* @className ZtUsacInDto
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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.lib.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.lib.session.dto
* @className ZtUsacOutDto
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </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.lib.session.presentation;
import io.micrometer.common.util.StringUtils;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.BizException;
import io.shinhanlife.glow.GlowControllerId;
import io.shinhanlife.glow.ResponseUtil;
import io.shinhanlife.dap.lib.session.converter.ZtUsacConverter;
import io.shinhanlife.dap.lib.session.domain.usecase.ZtUsacUseCase;
import io.shinhanlife.dap.lib.session.dto.SessionDto;
import io.shinhanlife.dap.lib.session.dto.ZtUsacInDto;
import io.shinhanlife.dap.lib.session.dto.ZtUsacOutDto;
import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Objects;
/**
* @package io.shinhanlife.dap.lib.session.presentation
* @className SsoRestController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/sso")
public class SsoRestController {
private static final String NLS_LOGIN_URL = "";
private final ZtUsacUseCase 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.lib.session.presentation.io;
import io.shinhanlife.dap.lib.session.dto.SessionDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.lib.session.presentation.io
* @className SsoResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SsoResponse {
private String retCode;
private SessionDto userInfo;
private String redirectUrl;
}

View File

@@ -330,7 +330,7 @@ public class ToolScaffolder {
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.usecase.%sUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import %s.converter.%sConverter;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.lib.dto;
package io.shinhanlife.dap.mcc.dto;
import lombok.Getter;
import lombok.Setter;
@@ -12,6 +12,7 @@ import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.HashSet;
import io.shinhanlife.dap.lib.dto.OperationType;
/**
* Tool(Agent) 명세 라우팅 정보를 담고 있는 메타데이터 클래스
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan) 사용됩니다.

View File

@@ -21,8 +21,8 @@ import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.lib.usecase.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import java.lang.reflect.Method;
import java.util.ArrayList;

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.lib.usecase;
package io.shinhanlife.dap.mcc.usecase;
import io.shinhanlife.dap.lib.adapter.connector.LegacyEimsConnector;
import io.shinhanlife.dap.lib.adapter.util.PiiMaskingUtils;

View File

@@ -6,7 +6,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.dto.ToolMetadata;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;

View File

@@ -11,7 +11,7 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
import io.shinhanlife.dap.lib.usecase.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import java.lang.reflect.Method;
import java.util.Map;
import org.junit.jupiter.api.Test;

View File

@@ -16,7 +16,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
* </pre>
*/
import io.shinhanlife.dap.mcc.biz.cmm.usecase.BalanceUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.biz.cmm.dto.BalanceRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.BillingProcessUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.biz.cmm.dto.BillingProcessRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.BillingStatusRequest;

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.BondIssueUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.biz.cmm.dto.BondCheckRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.BondIssueRequest;

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.CommonUtilityUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.biz.cmm.dto.LeaveCountRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.VacationRegisterRequest;

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ContractInquiryUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ContractDetailRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ContractStatusRequest;

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.CustomerInfoUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerDetailRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGradeRequest;

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.TemplateUtilityUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.biz.cmm.dto.TemplateDownloadRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase.impl;
import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse;
import io.shinhanlife.dap.mcc.biz.smp.usecase.DailyQuoteToolUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase.impl;
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
import io.shinhanlife.dap.mcc.biz.smp.usecase.ExchangeRateToolUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

View File

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase.impl;
import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberResponse;
import io.shinhanlife.dap.mcc.biz.smp.usecase.TeamMemberUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase.impl;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.mcc.biz.smp.usecase.WeatherToolUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.biz.smp.dto.WeatherRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.WeatherResponse;
import lombok.extern.slf4j.Slf4j;

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.sms.usecase.impl;
import io.shinhanlife.dap.mcc.biz.sms.usecase.SmsToolUseCase;
import io.shinhanlife.dap.lib.usecase.AbstractMcpToolUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import org.springframework.stereotype.Service;
import io.shinhanlife.dap.mcc.biz.sms.converter.SmsLegacyConverter;
import io.shinhanlife.dap.mcc.biz.sms.dto.SmsSendRequest;

606
manifest_output.json Normal file
View File

@@ -0,0 +1,606 @@
{
"bundleId": "tool-oth",
"revision": "1785826298946",
"tools": [
{
"name": "balance",
"endpoint": "http://localhost:8084/mcp/balance",
"title": "balance 툴",
"description": "고객의 계좌 잔액을 조회합니다.",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"accountNumber": {
"type": "string",
"pattern": "\\S",
"description": "고객의 계좌번호 (- 제외) "
}
},
"required": [
"accountNumber"
]
},
"annotations": {
"title": "balance 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "billing_process",
"endpoint": "http://localhost:8084/mcp/billing_process",
"title": "process 툴",
"description": "청구 처리",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"approvalStatus": {
"type": "string",
"description": "심사 승인 여부 (예: APPROVE, REJECT)",
"enum": [
"APPROVE",
"REJECT"
]
},
"billingId": {
"type": "string",
"pattern": "\\S",
"description": "처리할 청구 접수 번호"
}
},
"required": [
"billingId",
"approvalStatus"
]
},
"annotations": {
"title": "process 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "bond_issue",
"endpoint": "http://localhost:8084/mcp/bond_issue",
"title": "issue 툴",
"description": "증권 발행 í\u0085ŒìŠ¤íŠ¸1",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"amount": {
"type": "integer",
"description": "발행할 디지털 증권 금액",
"minimum": 1
},
"targetAccount": {
"type": "string",
"pattern": "\\S",
"description": "발행 대상 계좌 번호"
}
},
"required": [
"amount",
"targetAccount"
]
},
"annotations": {
"title": "issue 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "contract_detail",
"endpoint": "http://localhost:8084/mcp/contract_detail",
"title": "contract_detail 툴",
"description": "계약상세 조회",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"contractId": {
"type": "string",
"description": "조회할 계약 번호"
},
"customerName": {
"type": "string",
"description": "고객ëª\u0085"
}
},
"required": [
"customerName",
"contractId"
]
},
"annotations": {
"title": "contract_detail 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "customer_detail",
"endpoint": "http://localhost:8084/mcp/customer_detail",
"title": "detail 툴",
"description": "고객상세 정보 조회",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"customerId": {
"type": "string",
"description": "고객 식별 번호 (CID)"
},
"customerName": {
"type": "string",
"description": "고객ëª\u0085"
}
},
"required": [
"customerName"
]
},
"annotations": {
"title": "detail 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "daily_quote",
"endpoint": "http://localhost:8084/mcp/daily_quote",
"title": "랜덤 ëª\u0085언 툴",
"description": "무작위로 영감을 주는 ëª\u0085언을 하나 가져옵니다.",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"category": {
"type": "string",
"description": "category"
}
}
},
"annotations": {
"title": "랜덤 ëª\u0085언 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "exchange_rate",
"endpoint": "http://localhost:8084/mcp/exchange_rate",
"title": "실시간 환율 조회 툴",
"description": "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"currencyCode": {
"type": "string",
"description": "currencyCode"
}
}
},
"annotations": {
"title": "실시간 환율 조회 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "get_leave_count",
"endpoint": "http://localhost:8084/mcp/get_leave_count",
"title": "get_leave_count 툴",
"description": "연차 갯수 조회",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"employeeId": {
"type": "string",
"description": "연차 내역을 조회할 사원 번호"
}
},
"required": [
"employeeId"
]
},
"annotations": {
"title": "get_leave_count 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "get_smp_members",
"endpoint": "http://localhost:8084/mcp/get_smp_members",
"title": "신한라이프 MCP, TOOL 파트 구성원 조회",
"description": "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"teamName": {
"type": "string",
"description": "조회할 팀 이름 (예: AX, MCP, TOOL, 전체 등)"
}
}
},
"annotations": {
"title": "신한라이프 MCP, TOOL 파트 구성원 조회",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "get_template_file_url",
"endpoint": "http://localhost:8084/mcp/get_template_file_url",
"title": "í\u0085œí”Œë¦¿ 유틸리티",
"description": "요청한 í\u0085œí”Œë¦¿(엑ì\u0085€, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스í\u0085œ URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"templateId": {
"type": "string",
"description": "templateId"
}
}
},
"annotations": {
"title": "í\u0085œí”Œë¦¿ 유틸리티",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "metaCommonCode",
"endpoint": "http://localhost:8084/mcp/metaCommonCode",
"title": "메타 통합코드 조회 툴",
"description": "메타 통합코드 목록을 조회해줘",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"codeName": {
"type": "string",
"description": "코드ëª\u0085 검색 키워드 (예: 사용, 상태)"
},
"useYn": {
"type": "string",
"description": "사용여부 (예: Y, N)"
},
"groupCode": {
"type": "string",
"description": "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)"
}
}
},
"annotations": {
"title": "메타 통합코드 조회 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "metaTable",
"endpoint": "http://localhost:8084/mcp/metaTable",
"title": "메타 í\u0085Œì´ë¸” 조회 툴",
"description": "메타 í\u0085Œì´ë¸” 정보 목록을 조회해줘",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"tableLogicalName": {
"type": "string",
"description": "í\u0085Œì´ë¸” ë\u0085¼ë¦¬ëª\u0085(한글) 키워드 (예: 고객기본, 계약)"
},
"owner": {
"type": "string",
"description": "스키마/소유자ëª\u0085 (예: DAPADM, SHLOWN)"
},
"tableName": {
"type": "string",
"description": "í\u0085Œì´ë¸” 물리ëª\u0085 키워드 (예: TB_CUST_BAS, TB_CONT)"
}
}
},
"annotations": {
"title": "메타 í\u0085Œì´ë¸” 조회 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "sample.claim.search.resource",
"endpoint": "http://localhost:8084/mcp/sample.claim.search.resource",
"title": "Claim search JSON Schema sample",
"description": "Claim search Tool sample using input and output JSON Schema resources.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"claimNo": {
"type": "string",
"description": "청구번호. CLM 다음 숫자 13자리 형식이다.",
"pattern": "^CLM[0-9]{13}$",
"examples": [
"CLM2026070100123"
]
},
"contractNo": {
"type": "string",
"description": "계약번호. 숫자 11자리 형식이다.",
"pattern": "^[0-9]{11}$",
"examples": [
"10023456789"
]
},
"status": {
"type": "string",
"enum": [
"RECEIVED",
"REVIEWING",
"ADDITIONAL_DOC_REQUIRED",
"APPROVED",
"PAID",
"REJECTED",
"WITHDRAWN"
]
},
"size": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 20
}
},
"required": [
],
"additionalProperties": false,
"anyOf": [
{
"required": [
"claimNo"
]
},
{
"required": [
"contractNo"
]
}
]
},
"annotations": {
"title": "Claim search JSON Schema sample",
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "secret_tool",
"endpoint": "http://localhost:8084/mcp/secret_tool",
"title": "secret_tool 툴",
"description": "비공개 툴 í\u0085ŒìŠ¤íŠ¸",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"employeeId": {
"type": "string",
"description": "연차 내역을 조회할 사원 번호"
}
},
"required": [
"employeeId"
]
},
"annotations": {
"title": "secret_tool 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "solReqDetail",
"endpoint": "http://localhost:8084/mcp/solReqDetail",
"title": "SolReqDetail 툴",
"description": "SOL 의뢰서 상세 조회",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"srId": {
"type": "string",
"description": "상세 조회할 SOL 의뢰서 ID"
}
},
"required": [
"srId"
]
},
"annotations": {
"title": "SolReqDetail 툴",
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "solReqList",
"endpoint": "http://localhost:8084/mcp/solReqList",
"title": "SolReqList 툴",
"description": "SOL 의뢰서 목록 조회해줘",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "조회기간 (예: 1개월, 3개월 등)"
},
"status": {
"type": "string",
"description": "진행상태 (예: 진행중, 완료 등)"
},
"target": {
"type": "string",
"description": "조회대상 (예: 나의 ì—\u0085무, 전체 등)"
}
}
},
"annotations": {
"title": "SolReqList 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
},
{
"name": "weather",
"endpoint": "http://localhost:8084/mcp/weather",
"title": "날씨 조회 툴",
"description": "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.",
"inputSchema": {
"additionalProperties": false,
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "날씨를 조회할 도시 이름 (예: 서울, 부산, 제주)"
}
},
"required": [
"city"
]
},
"annotations": {
"title": "날씨 조회 툴",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
},
"_meta": {
"version": "1.0.0",
"timeoutMillis": 300000,
"enabled": true
}
}
]
}

715
recent_changes.diff Normal file
View File

@@ -0,0 +1,715 @@
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
index c7251dd..6ee5f33 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpFunction.java
@@ -39,6 +39,17 @@ public @interface McpFunction {
*/
// 異붽?: Redis ?먮룞 ?깅줉 諛?Heartbeat ?€???щ? ?쒖뼱
String inputSchemaResource() default "";
+
+ /**
+ * Tool response JSON Schema. When unset, output validation is skipped.
+ */
+ String outputSchema() default "{}";
+
+ /**
+ * Classpath resource for a complex Tool response JSON Schema.
+ * This value has priority over outputSchema.
+ */
+ String outputSchemaResource() default "";
boolean register() default false;
// 異붽?: ??紐⑸줉 ?몄텧 ?щ? ?쒖뼱 (false ???쇱슦?낆? ?섎굹 紐⑸줉?먯꽌 ?④?)
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java
new file mode 100644
index 0000000..cfb37dc
--- /dev/null
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpOutputSchema.java
@@ -0,0 +1,17 @@
+package io.shinhanlife.dap.lib.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a Tool response DTO for automatic output JSON Schema generation.
+ * Field constraints are declared with {@link McpValidation}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface McpOutputSchema {
+}
\ No newline at end of file
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
index ecf23c8..4353edf 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/annotation/McpValidation.java
@@ -31,6 +31,6 @@ public @interface McpValidation {
int maxLength() default -1;
String[] allowedValues() default {};
String format() default "";
- String defaultValue() default "";
+ boolean nullable() default false; String defaultValue() default "";
String[] examples() default {};
}
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
index a90efa0..d95dae6 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/JsonSchemaGenerator.java
@@ -103,6 +103,14 @@ public class JsonSchemaGenerator {
if (validation != null && validation.examples().length > 0) {
fieldSchema.put("examples", List.of(validation.examples()));
}
+ if (validation != null && validation.nullable()) {
+ Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
+ fieldSchema = new HashMap<>();
+ fieldSchema.put("anyOf", List.of(
+ nonNullSchema,
+ Map.of("type", "null")
+ ));
+ }
properties.put(field.getName(), fieldSchema);
}
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
index 787cbc1..5ea54df 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/lib/util/ToolSchemaResolver.java
@@ -3,7 +3,7 @@ package io.shinhanlife.dap.lib.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
-import java.io.InputStream;
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream;
import java.util.Map;
import org.springframework.core.io.ClassPathResource;
@@ -27,19 +27,44 @@ public class ToolSchemaResolver {
return JsonSchemaGenerator.generateSchema(requestType);
}
+ /**
+ * Resolves an explicitly declared response schema.
+ * Response schemas are opt-in so existing tools keep their current response behavior.
+ */
+ public Map<String, Object> resolveOutput(McpFunction function, Class<?> responseType) {
+ if (function != null && !function.outputSchemaResource().isBlank()) {
+ return loadResource(function.outputSchemaResource());
+ }
+ if (function != null && !function.outputSchema().isBlank()
+ && !"{}".equals(function.outputSchema().trim())) {
+ return parse(function.outputSchema(), "McpFunction.outputSchema");
+ }
+ if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) {
+ return JsonSchemaGenerator.generateSchema(responseType);
+ }
+ return Map.of();
+ }
+
+ /**
+ * Retained for callers that use only explicit output schemas.
+ */
+ public Map<String, Object> resolveOutput(McpFunction function) {
+ return resolveOutput(function, null);
+ }
+
private Map<String, Object> loadResource(String location) {
String path = location.startsWith("classpath:")
? location.substring("classpath:".length())
: location;
ClassPathResource resource = new ClassPathResource(path);
if (!resource.exists()) {
- throw new IllegalStateException("MCP input schema resource not found: " + location);
+ throw new IllegalStateException("MCP schema resource not found: " + location);
}
try (InputStream inputStream = resource.getInputStream()) {
return objectMapper.readValue(inputStream, new TypeReference<>() { });
} catch (Exception e) {
- throw new IllegalStateException("Failed to load MCP input schema resource: " + location, e);
+ throw new IllegalStateException("Failed to load MCP schema resource: " + location, e);
}
}
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
index daf0018..ec840ed 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java
@@ -192,14 +192,30 @@ public class BusinessToolController {
} else {
methodResult = targetMethod.invoke(targetBean, invokeArgument);
}
+
+ Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType());
+ if (!outputSchema.isEmpty()) {
+ List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
+ if (!outputErrors.isEmpty()) {
+ log.error("[Tool] Output schema validation failed. tool={}, errors={}",
+ functionName, outputErrors);
+ Map<String, Object> errorBody = new HashMap<>();
+ errorBody.put("code", "INVALID_TOOL_RESPONSE");
+ errorBody.put("message", "Tool response does not match its output schema");
+ if (finalRequestId != null) {
+ errorBody.put("request_id", finalRequestId);
+ }
+ return ResponseEntity.internalServerError().body(errorBody);
+ }
+ }
long elapsed = System.currentTimeMillis() - startTime;
// 5. 寃곌낵 諛섑솚 (?쒖닔 REST ?묐떟)
try {
- log.info("[Tool -> MCP Gateway] ?숈쟻 ???ㅽ뻾 寃곌낵 諛섑솚: {}", objectMapper.writeValueAsString(methodResult));
+ log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
} catch (Exception e) {
- log.info("[Tool -> MCP Gateway] ?숈쟻 ???ㅽ뻾 寃곌낵 諛섑솚: {}", methodResult);
+ log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult);
}
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
diff --git a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
index 876144c..59c68ff 100644
--- a/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
+++ b/dap-tool-core/src/main/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidator.java
@@ -21,8 +21,12 @@ public class ToolArgumentSchemaValidator {
}
public List<Error> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
+ return validateValue(schemaDefinition, arguments);
+ }
+
+ public List<Error> validateValue(Map<String, Object> schemaDefinition, Object value) throws Exception {
SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7);
Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition));
- return schema.validate(objectMapper.writeValueAsString(arguments), InputFormat.JSON);
+ return schema.validate(objectMapper.writeValueAsString(value), InputFormat.JSON);
}
}
diff --git a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
index 6737a00..bc05f05 100644
--- a/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
+++ b/dap-tool-core/src/test/java/io/shinhanlife/dap/lib/util/ToolSchemaResolverTest.java
@@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
+import io.shinhanlife.dap.lib.annotation.McpValidation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
@@ -15,66 +17,103 @@ class ToolSchemaResolverTest {
private final ToolSchemaResolver resolver = new ToolSchemaResolver(new ObjectMapper());
@Test
- void usesExplicitSchemaResourceBeforeAutomaticDtoSchema() throws Exception {
- Method method = SchemaBackedTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
- McpFunction function = method.getAnnotation(McpFunction.class);
+ void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
+ Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
- Map<String, Object> schema = resolver.resolve(function, AutoGeneratedRequest.class);
+ Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
assertEquals(false, schema.get("additionalProperties"));
- assertTrue(schema.containsKey("anyOf"));
- assertEquals(50, ((Map<?, ?>) ((Map<?, ?>) schema.get("properties")).get("size")).get("maximum"));
- assertEquals(List.of(Map.of("required", List.of("claimNo"))), schema.get("anyOf"));
+ assertTrue(!properties(schema).containsKey("differentField"));
}
+ @Test
+ void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
+ Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
- /* Inline schema resolution is covered by the same resolver branch at integration level.
- void usesInlineSchemaBeforeAutomaticDtoSchema() throws Exception {
- Method method = InlineSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
+ Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutomaticRequest.class);
- Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
+ assertTrue(properties(schema).containsKey("differentField"));
+ }
- assertEquals("object", schema.get("type"));
+ @Test
+ void resolvesExplicitOutputSchema() throws Exception {
+ Method method = OutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
+ Map<String, Object> schema = resolver.resolveOutput(method.getAnnotation(McpFunction.class));
assertEquals(false, schema.get("additionalProperties"));
- assertTrue(!((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
+ assertTrue(properties(schema).containsKey("resultCode"));
}
- */
@Test
- void generatesSchemaFromRequestDtoWhenNoExplicitSchemaIsConfigured() throws Exception {
- Method method = SchemaBackedTool.AutomaticSchemaTool.class.getDeclaredMethod("search", AutoGeneratedRequest.class);
+ void generatesOutputSchemaFromMarkedResponseDto() throws Exception {
+ Method method = AutomaticOutputSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
- Map<String, Object> schema = resolver.resolve(method.getAnnotation(McpFunction.class), AutoGeneratedRequest.class);
+ Map<String, Object> schema = resolver.resolveOutput(
+ method.getAnnotation(McpFunction.class), SimpleResponse.class);
- assertTrue(((Map<?, ?>) schema.get("properties")).containsKey("differentField"));
+ assertEquals(List.of("resultCode"), schema.get("required"));
+ assertEquals(List.of("SUCCESS", "FAILURE"), property(schema, "resultCode").get("enum"));
}
- static class InlineSchemaTool {
+ @Test
+ void doesNotEnableOutputValidationWhenOutputSchemaIsNotDeclared() throws Exception {
+ Method method = AutomaticSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
+ Map<String, Object> schema = resolver.resolveOutput(
+ method.getAnnotation(McpFunction.class), AutomaticRequest.class);
+ assertTrue(schema.isEmpty());
+ }
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> properties(Map<String, Object> schema) {
+ return (Map<String, Object>) schema.get("properties");
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map<String, Object> property(Map<String, Object> schema, String name) {
+ return (Map<String, Object>) properties(schema).get(name);
+ }
- @McpFunction(displayName = "inline", name = "sample.inline", description = "inline", inputSchema = "{\\\"type\\\":\\\"object\\\",\\\"properties\\\":{},\\\"additionalProperties\\\":false}")
- void search(AutoGeneratedRequest request) {
+ static class InlineSchemaTool {
+ @McpFunction(
+ displayName = "inline",
+ name = "sample.inline",
+ description = "inline schema",
+ inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}")
+ void search(AutomaticRequest request) {
}
}
- static class SchemaBackedTool {
static class AutomaticSchemaTool {
+ @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic schema")
+ void search(AutomaticRequest request) {
+ }
+ }
- @McpFunction(displayName = "automatic", name = "sample.automatic", description = "automatic")
- void search(AutoGeneratedRequest request) {
+ static class AutomaticOutputSchemaTool {
+ @McpFunction(displayName = "automatic-output", name = "sample.automatic-output", description = "automatic output")
+ SimpleResponse search(AutomaticRequest request) {
+ return null;
}
}
+ @McpOutputSchema
+ static class SimpleResponse {
+ @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
+ private String resultCode;
+
+ @McpValidation(maxLength = 200)
+ private String message;
+ }
+ static class OutputSchemaTool {
@McpFunction(
- displayName = "泥?뎄 議고쉶",
- name = "processing.claim.search",
- description = "蹂댄뿕湲?泥?뎄瑜?議고쉶?쒕떎.",
- inputSchemaResource = "classpath:tool-schemas/claim-search-input-schema.json")
- void search(AutoGeneratedRequest request) {
+ displayName = "output",
+ name = "sample.output",
+ description = "output schema",
+ outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}")
+ void search(AutomaticRequest request) {
}
}
- static class AutoGeneratedRequest {
+ static class AutomaticRequest {
private String differentField;
}
}
diff --git a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json b/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json
deleted file mode 100644
index e595b42..0000000
--- a/dap-tool-core/src/test/resources/tool-schemas/claim-search-input-schema.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "type": "object",
- "properties": {
- "claimNo": {
- "type": "string",
- "pattern": "^CLM[0-9]{13}$"
- },
- "size": {
- "type": "integer",R
- "minimum": 1,
- "maximum": 50,
- "default": 20
- }
- },
- "additionalProperties": false,
- "anyOf": [
- { "required": ["claimNo"] }
- ]
-}
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java
new file mode 100644
index 0000000..abd2d2d
--- /dev/null
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchResponse.java
@@ -0,0 +1,88 @@
+package io.shinhanlife.dap.mcc.biz.cmm.dto;
+
+import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
+import io.shinhanlife.dap.lib.annotation.McpParameter;
+import io.shinhanlife.dap.lib.annotation.McpValidation;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Claim search response sample.
+ * Complex response rules are defined by outputSchemaResource; annotations document
+ * the same simple field constraints for automatic schema generation examples.
+ */
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@McpOutputSchema
+public class ClaimSearchResponse {
+
+ @McpParameter(description = "Execution result code.")
+ @McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
+ private String resultCode;
+
+ @McpParameter(description = "User-readable label for resultCode.")
+ @McpValidation(required = true, maxLength = 100)
+ private String resultLabel;
+
+ @McpParameter(description = "Current claim processing status code.")
+ @McpValidation(required = true, allowedValues = {
+ "RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
+ "APPROVED", "PAID", "REJECTED", "WITHDRAWN"
+ })
+ private String status;
+
+ @McpParameter(description = "User-readable label for status.")
+ @McpValidation(required = true, maxLength = 100)
+ private String statusLabel;
+
+ @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
+ @McpValidation(minimum = 0, nullable = true)
+ private Long approvedAmount;
+
+ @McpParameter(description = "Present only when status is REJECTED; otherwise null.")
+ @McpValidation(maxLength = 200, nullable = true)
+ private String rejectionReason;
+
+ @McpParameter(description = "Claim summaries, ordered by received date descending.")
+ @McpValidation(required = true)
+ private List<ClaimSummary> items;
+
+ @McpParameter(description = "True when additional results exist beyond this response.")
+ @McpValidation(required = true)
+ private Boolean hasMore;
+
+ @McpParameter(description = "Total number of matched claims.")
+ @McpValidation(required = true, minimum = 0)
+ private Integer totalCount;
+
+ @Getter
+ @Setter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class ClaimSummary {
+
+ @McpParameter(description = "Claim processing status code.")
+ @McpValidation(required = true)
+ private String status;
+
+ @McpParameter(description = "User-readable label for status.")
+ @McpValidation(required = true)
+ private String statusLabel;
+
+ @McpParameter(description = "Received date in YYYY-MM-DD format.")
+ @McpValidation(required = true, format = "date")
+ private String receivedDate;
+
+ @McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
+ @McpValidation(minimum = 0, nullable = true)
+ private Long approvedAmount;
+ }
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
index 16d1aa0..9a55c1a 100644
--- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java
@@ -3,6 +3,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
+import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
@McpTool(
routingType = "DIRECT",
@@ -12,13 +13,14 @@ public interface ClaimSearchSchemaSampleUseCase {
@McpFunction(
register = false,
- displayName = "泥?뎄 議고쉶 JSON Schema ?섑뵆",
+ displayName = "Claim search JSON Schema sample",
name = "sample.claim.search.resource",
- description = "inputSchemaResource瑜??ъ슜?섎뒗 泥?뎄 議고쉶 Tool ?섑뵆?낅땲??",
- prompt = "泥?뎄踰덊샇 ?먮뒗 怨꾩빟踰덊샇濡?蹂댄뿕湲?泥?뎄瑜?議고쉶?댁쨾.",
- inputSchemaResource = "classpath:tool-schemas/claim-search-resource-input-schema.json",
+ description = "Claim search Tool sample using input and output JSON Schema resources.",
+ prompt = "Search an insurance claim by claim number or contract number.",
+ inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
+ outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json",
readOnlyHint = true,
idempotentHint = true
)
- Object search(ClaimSearchRequest request);
-}
+ ClaimSearchResponse search(ClaimSearchRequest request);
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
index e6cd98b..1a817f5 100644
--- a/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
+++ b/dap-tool-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchSchemaSampleUseCaseImpl.java
@@ -1,22 +1,36 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
+import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
-import java.util.Map;
+import java.util.List;
import org.springframework.stereotype.Service;
/**
- * inputSchemaResource ?곸슜 諛⑹떇??蹂댁뿬二쇰뒗 鍮꾨끂異??섑뵆 Tool?대떎.
- * ?ㅼ젣 MCI/EIMS ?곕룞?€ 異붽??섏? ?딅뒗??
+ * Non-exposed sample Tool. It does not call MCI or EIMS.
*/
@Service
public class ClaimSearchSchemaSampleUseCaseImpl implements ClaimSearchSchemaSampleUseCase {
@Override
- public Object search(ClaimSearchRequest request) {
- return Map.of(
- "message", "inputSchemaResource JSON Schema sample",
- "request", request == null ? Map.of() : request
- );
+ public ClaimSearchResponse search(ClaimSearchRequest request) {
+ ClaimSearchResponse.ClaimSummary item = ClaimSearchResponse.ClaimSummary.builder()
+ .status("REVIEWING")
+ .statusLabel("Under review")
+ .receivedDate("2026-08-04")
+ .approvedAmount(null)
+ .build();
+
+ return ClaimSearchResponse.builder()
+ .resultCode("SUCCESS")
+ .resultLabel("Success")
+ .status("REVIEWING")
+ .statusLabel("Under review")
+ .approvedAmount(null)
+ .rejectionReason(null)
+ .items(List.of(item))
+ .hasMore(false)
+ .totalCount(1)
+ .build();
}
-}
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json
similarity index 100%
rename from dap-tool-oth/src/main/resources/tool-schemas/claim-search-resource-input-schema.json
rename to dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-input-schema.json
diff --git a/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json
new file mode 100644
index 0000000..f94c661
--- /dev/null
+++ b/dap-tool-oth/src/main/resources/tool-schemas/cmm/claim-search-resource-output-schema.json
@@ -0,0 +1,81 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "description": "Claim search response. This schema intentionally excludes employee identifiers, customer names, contact details, account information, and other PII.",
+ "properties": {
+ "resultCode": {
+ "type": "string",
+ "enum": ["SUCCESS", "FAILURE"],
+ "description": "Machine-readable execution result code."
+ },
+ "resultLabel": {
+ "type": "string",
+ "description": "User-readable label for resultCode."
+ },
+ "status": {
+ "type": "string",
+ "enum": ["RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED", "APPROVED", "PAID", "REJECTED", "WITHDRAWN"],
+ "description": "Current claim processing status code."
+ },
+ "statusLabel": {
+ "type": "string",
+ "description": "User-readable label for status."
+ },
+ "approvedAmount": {
+ "anyOf": [
+ { "type": "number", "minimum": 0 },
+ { "type": "null" }
+ ],
+ "description": "Approved amount. It is null before review and must not be interpreted as zero."
+ },
+ "rejectionReason": {
+ "type": ["string", "null"],
+ "maxLength": 200,
+ "description": "Has a value only when status is REJECTED. It is null for every other status."
+ },
+ "items": {
+ "type": "array",
+ "description": "Claim summaries ordered by receivedDate descending. No personally identifiable information is included.",
+ "items": {
+ "type": "object",
+ "properties": {
+ "status": { "type": "string", "description": "Claim status code." },
+ "statusLabel": { "type": "string", "description": "User-readable label for status." },
+ "receivedDate": { "type": "string", "format": "date", "description": "Claim received date." },
+ "approvedAmount": {
+ "anyOf": [
+ { "type": "number", "minimum": 0 },
+ { "type": "null" }
+ ],
+ "description": "Null before review; do not interpret as zero."
+ }
+ },
+ "required": ["status", "statusLabel", "receivedDate"],
+ "additionalProperties": false
+ }
+ },
+ "hasMore": {
+ "type": "boolean",
+ "description": "True when additional results exist beyond this response."
+ },
+ "totalCount": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Total number of matched claims."
+ }
+ },
+ "required": ["resultCode", "resultLabel", "status", "statusLabel", "items", "hasMore", "totalCount"],
+ "allOf": [
+ {
+ "if": {
+ "properties": { "status": { "const": "REJECTED" } },
+ "required": ["status"]
+ },
+ "then": {
+ "properties": { "rejectionReason": { "type": "string", "minLength": 1 } },
+ "required": ["rejectionReason"]
+ }
+ }
+ ],
+ "additionalProperties": false
+}
\ No newline at end of file
diff --git a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
index b52e10c..cead062 100644
--- a/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
+++ b/dap-tool-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java
@@ -3,7 +3,13 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.util.JsonSchemaGenerator;
+import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
+import io.shinhanlife.dap.mcc.biz.cmm.usecase.impl.ClaimSearchSchemaSampleUseCaseImpl;
+import io.shinhanlife.dap.mcc.presentation.ToolArgumentSchemaValidator;import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;
+import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -24,8 +30,59 @@ class ClaimSearchRequestSchemaTest {
Map.of("required", List.of("contractNo"))), schema.get("anyOf"));
}
+ @Test
+ void resolvesSchemaFromToolModuleResource() throws Exception {
+ Method method = ClaimSearchSchemaSampleUseCase.class
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
+ McpFunction function = method.getAnnotation(McpFunction.class);
+
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper())
+ .resolve(function, ClaimSearchRequest.class);
+
+ assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
+ assertTrue(schema.containsKey("anyOf"));
+ assertEquals(50, property(schema, "size").get("maximum"));
+ }
+
+ @Test
+ void resolvesOutputSchemaFromToolModuleResource() throws Exception {
+ Method method = ClaimSearchSchemaSampleUseCase.class
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
+ McpFunction function = method.getAnnotation(McpFunction.class);
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper()).resolveOutput(function);
+ assertEquals("https://json-schema.org/draft/2020-12/schema", schema.get("$schema"));
+ assertTrue(properties(schema).containsKey("resultCode"));
+ assertTrue(properties(schema).containsKey("statusLabel"));
+ assertTrue(properties(schema).containsKey("hasMore"));
+ assertTrue(schema.containsKey("allOf"));
+ }
+
+ @Test
+ void sampleResponseConformsToOutputSchema() throws Exception {
+ Method method = ClaimSearchSchemaSampleUseCase.class
+ .getDeclaredMethod("search", ClaimSearchRequest.class);
+ Map<String, Object> schema = new ToolSchemaResolver(new ObjectMapper())
+ .resolveOutput(method.getAnnotation(McpFunction.class));
+
+ ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
+ ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
+
+ assertTrue(validator.validateValue(schema, response).isEmpty());
+ }
+ @Test
+ void sampleResponseConformsToAutomaticallyGeneratedOutputSchema() throws Exception {
+ Map<String, Object> schema = JsonSchemaGenerator.generateSchema(ClaimSearchResponse.class);
+ ClaimSearchResponse response = new ClaimSearchSchemaSampleUseCaseImpl().search(new ClaimSearchRequest());
+ ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
+
+ assertTrue(validator.validateValue(schema, response).isEmpty());
+ }
@SuppressWarnings("unchecked")
private Map<String, Map<String, Object>> properties(Map<String, Object> schema) {
return (Map<String, Map<String, Object>>) schema.get("properties");
}
+
+ private Map<String, Object> property(Map<String, Object> schema, String name) {
+ return properties(schema).get(name);
+ }
}