forked from kimhyungsik/ax_hub_mcp_tool
feat: update MCP contracts and integrations
All checks were successful
Deploy Tools / deploy (push) Successful in 1m34s
All checks were successful
Deploy Tools / deploy (push) Successful in 1m34s
This commit is contained in:
34
README.md
34
README.md
@@ -98,10 +98,18 @@ DATMS
|
||||
```powershell
|
||||
$headers = @{
|
||||
'X-Tool-Server-API-Key' = $env:TOOL_SERVER_API_KEY
|
||||
'guid' = 'guid-local-001'
|
||||
'x-request-id' = 'request-local-001'
|
||||
'employee-no' = '100001'
|
||||
'virtual-employee-no' = 'V100001'
|
||||
'X-Guid' = 'guid-local-001'
|
||||
'X-Praf-No' = '100001'
|
||||
'X-Request-Id' = 'request-local-001'
|
||||
'X-Request-Time' = '2026-08-25T12:34:56+09:00'
|
||||
'X-Vrtl-Praf-No' = 'V100001'
|
||||
'X-App-Code' = 'DATMT'
|
||||
'X-Project-Code' = 'AXHUB'
|
||||
'X-User-Ip' = '10.0.0.10'
|
||||
'X-Caller-Ip' = '10.0.0.20'
|
||||
'X-Caller-Host' = 'caller.example.internal'
|
||||
'X-Channel' = 'MCP'
|
||||
'X-Agent-Id' = 'agent-local-001'
|
||||
'mcp-session-id' = 'session-local-001'
|
||||
}
|
||||
|
||||
@@ -120,14 +128,24 @@ DATMS가 DATMT Tool Service를 호출할 때 사용하는 헤더는 다음과
|
||||
| 헤더 | 필수 여부 | 용도 | 전달 동작 |
|
||||
|---|---|---|---|
|
||||
| `X-Tool-Server-API-Key` | 인증 설정 시 필수 | DATMS와 DATMT 사이의 Tool Server 인증 | `mcp.security.api-key` 또는 `api-keys`와 비교 |
|
||||
| `guid` | 선택 | 업무 호출 상관관계 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 |
|
||||
| `x-request-id` | 선택 | 요청 추적 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 |
|
||||
| `employee-no` | 선택 | 실제 사용자 사번 | 하위 HTTP 호출로 전달 |
|
||||
| `virtual-employee-no` | 선택 | 가상 사용자 사번 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Guid` | 선택 | 업무 호출 상관관계 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 |
|
||||
| `X-Praf-No` | 선택 | 실제 사용자 사번 | 세션 조회와 하위 HTTP 호출로 전달 |
|
||||
| `X-Request-Id` | 선택 | 요청 추적 식별자 | 실행 로그, 성공 응답, 하위 HTTP 호출로 전달 |
|
||||
| `X-Request-Time` | 선택 | 요청 발생 시각 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Vrtl-Praf-No` | 선택 | 가상 사용자 사번 | 하위 HTTP 호출로 전달 |
|
||||
| `X-App-Code` | 선택 | 호출 애플리케이션 코드 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Project-Code` | 선택 | 호출 프로젝트 코드 | 하위 HTTP 호출로 전달 |
|
||||
| `X-User-Ip` | 선택 | 사용자 IP 주소 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Caller-Ip` | 선택 | 호출 시스템 IP 주소 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Caller-Host` | 선택 | 호출 시스템 호스트명 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Channel` | 선택 | 호출 채널 | 하위 HTTP 호출로 전달 |
|
||||
| `X-Agent-Id` | 선택 | 호출 Agent 식별자 | 하위 HTTP 호출로 전달 |
|
||||
| `mcp-session-id` | 선택 | MCP 세션 식별자 | 성공 응답과 하위 HTTP 호출로 전달 |
|
||||
|
||||
REST 경로 `/mcp/{toolName}`은 Controller가 위 헤더를 직접 읽습니다. MCP Streamable HTTP 경로 `/mcp`는 `McpRequestHeaderFilter`가 동일한 헤더를 `McpRequestHeaderContext`에 저장한 뒤 Tool 실행과 하위 HTTP 호출에 전달합니다. 헤더가 없는 하위 HTTP 호출에는 `X-ANONYMOUS-REQ: AXHUB-TOOL`이 설정됩니다.
|
||||
|
||||
기존 `guid`, `employee-no`, `virtual-employee-no` 헤더는 지원하지 않습니다.
|
||||
|
||||
주요 실행 응답은 다음과 같습니다.
|
||||
|
||||
| HTTP 상태 | 코드 | 의미 |
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package io.shinhanlife.dat.lib.adapter.test;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -39,10 +42,13 @@ public class MockEimsHttpServer {
|
||||
if (!resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
try {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
log.info("[MockEimsHttpServer] HTTP mock request. toolName={}, body={}", toolName, request);
|
||||
return ResponseEntity.ok(objectMapper.readTree(resource.getInputStream()));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(objectMapper.readTree(inputStream));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("[MockEimsHttpServer] Invalid mock response JSON. toolName={}", toolName, e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
} catch (IOException e) {
|
||||
log.warn("[MockEimsHttpServer] Unable to read mock response. toolName={}", toolName, e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public class ToolSlaMonitoringAspect {
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Throwable e) {
|
||||
} catch (RuntimeException e) {
|
||||
if (stopWatch.isRunning()) {
|
||||
stopWatch.stop();
|
||||
}
|
||||
|
||||
@@ -92,11 +92,19 @@ public class AxhubHttpComponent {
|
||||
if (inbound == null) {
|
||||
header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST);
|
||||
} else {
|
||||
putIfPresent(header, "x-request-id", inbound.requestId());
|
||||
putIfPresent(header, "guid", inbound.guid());
|
||||
putIfPresent(header, "X-Guid", inbound.guid());
|
||||
putIfPresent(header, "X-Praf-No", inbound.prafNo());
|
||||
putIfPresent(header, "X-Request-Id", inbound.requestId());
|
||||
putIfPresent(header, "X-Request-Time", inbound.requestTime());
|
||||
putIfPresent(header, "X-Vrtl-Praf-No", inbound.vrtlPrafNo());
|
||||
putIfPresent(header, "X-App-Code", inbound.appCode());
|
||||
putIfPresent(header, "X-Project-Code", inbound.projectCode());
|
||||
putIfPresent(header, "X-User-Ip", inbound.userIp());
|
||||
putIfPresent(header, "X-Caller-Ip", inbound.callerIp());
|
||||
putIfPresent(header, "X-Caller-Host", inbound.callerHost());
|
||||
putIfPresent(header, "X-Channel", inbound.channel());
|
||||
putIfPresent(header, "X-Agent-Id", inbound.agentId());
|
||||
putIfPresent(header, "mcp-session-id", inbound.mcpSessionId());
|
||||
putIfPresent(header, "employee-no", inbound.employeeNo());
|
||||
putIfPresent(header, "virtual-employee-no", inbound.virtualEmployeeNo());
|
||||
}
|
||||
header.setReadTimeout(timeout == 0 ? defaultReadTimeout() : timeout);
|
||||
return header;
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.shinhanlife.dat.lib.integration.mci.component;
|
||||
import io.shinhanlife.dat.lib.session.dto.SessionDto;
|
||||
import io.shinhanlife.dat.lib.util.SessionUtil;
|
||||
import io.shinhanlife.dat.lib.config.GlowCommunicationProperties;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import io.shinhanlife.glow.communication.dto.CommonHeader;
|
||||
import io.shinhanlife.glow.communication.dto.HeaderDefaults;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
@@ -91,12 +90,7 @@ public class AxhubMciComponent {
|
||||
CommonHeader reqHeader = (CommonHeader) request.getHeader();
|
||||
log.info("[AxhubMciComponent] {} MCI 호출시작 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
|
||||
|
||||
Transfer<O> response;
|
||||
try {
|
||||
response = (Transfer<O>) mci.sync(request);
|
||||
} catch (RuntimeException e) {
|
||||
throw new BizException("CST00477", new String[]{"대내 MCI 호출 결과 처리중 오류가 발생했습니다."}, e);
|
||||
}
|
||||
Transfer<O> response = (Transfer<O>) mci.sync(request);
|
||||
|
||||
log.info("[AxhubMciComponent] {} MCI 호출종료 (수신서비스: {})", reqHeader.getItrfId(), reqHeader.getRcvSvcId());
|
||||
|
||||
@@ -109,7 +103,7 @@ public class AxhubMciComponent {
|
||||
return response;
|
||||
}
|
||||
|
||||
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto) throws Exception {
|
||||
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto) {
|
||||
Map<HeaderDefaults, String> commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId);
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap);
|
||||
|
||||
@@ -132,7 +126,7 @@ public class AxhubMciComponent {
|
||||
* @param <I> inputDto 제너릭
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto, Class<O> resBodyClass) throws Exception {
|
||||
public <O, I> Transfer<O> callTo(String itrfName, String rcvSvcId, I inputDto, Class<O> resBodyClass) {
|
||||
Map<HeaderDefaults, String> commonHeaderMap = createCommonHeaderMap(itrfName, rcvSvcId);
|
||||
CommonHeader header = CommonHeaderFactory.createRequestHeader(commonHeaderMap);
|
||||
|
||||
@@ -153,9 +147,8 @@ public class AxhubMciComponent {
|
||||
* @return Transfer
|
||||
* @param <O> resBodyClass 제너릭
|
||||
* @param <I> inputDto 제너릭
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO, Class<O> resBodyClass) throws Exception {
|
||||
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO, Class<O> resBodyClass) {
|
||||
String className = inputDTO.getClass().getSimpleName();
|
||||
String rcvSvcId = className.replace("_I", "");
|
||||
return callTo(itrfName, rcvSvcId, inputDTO, resBodyClass);
|
||||
@@ -168,9 +161,8 @@ public class AxhubMciComponent {
|
||||
* @return Transfer
|
||||
* @param <O> resBodyClass 제너릭
|
||||
* @param <I> inputDto 제너릭
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO) throws Exception {
|
||||
public <O, I> Transfer<O> callTo(String itrfName, I inputDTO) {
|
||||
String className = inputDTO.getClass().getSimpleName();
|
||||
String rcvSvcId = className.replace("_I", "");
|
||||
return callTo(itrfName, rcvSvcId, inputDTO);
|
||||
|
||||
@@ -10,7 +10,7 @@ import io.shinhanlife.dat.lib.session.mci.AxhubSessionService;
|
||||
import io.shinhanlife.dat.lib.session.dto.SessionDto;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/** Captures optional correlation and employee headers for an MCP HTTP call and creates mock session. */
|
||||
/** Captures optional Tool request headers for an MCP HTTP call and creates the user session. */
|
||||
@Component
|
||||
public class McpRequestHeaderFilter implements Filter {
|
||||
|
||||
@@ -29,18 +29,26 @@ public class McpRequestHeaderFilter implements Filter {
|
||||
return;
|
||||
}
|
||||
|
||||
String empNo = request.getHeader("employee-no");
|
||||
String prafNo = request.getHeader("X-Praf-No");
|
||||
|
||||
McpRequestHeaderContext.set(new McpRequestHeaders(
|
||||
request.getHeader("x-request-id"),
|
||||
request.getHeader("guid"),
|
||||
request.getHeader("mcp-session-id"),
|
||||
empNo,
|
||||
request.getHeader("virtual-employee-no")));
|
||||
request.getHeader("X-Guid"),
|
||||
prafNo,
|
||||
request.getHeader("X-Request-Id"),
|
||||
request.getHeader("X-Request-Time"),
|
||||
request.getHeader("X-Vrtl-Praf-No"),
|
||||
request.getHeader("X-App-Code"),
|
||||
request.getHeader("X-Project-Code"),
|
||||
request.getHeader("X-User-Ip"),
|
||||
request.getHeader("X-Caller-Ip"),
|
||||
request.getHeader("X-Caller-Host"),
|
||||
request.getHeader("X-Channel"),
|
||||
request.getHeader("X-Agent-Id"),
|
||||
request.getHeader("mcp-session-id")));
|
||||
|
||||
// Fetch session info via MCI using employee-no (Only if sessionService is available, e.g. in Tool Pods)
|
||||
if (sessionService != null && empNo != null && !empNo.isEmpty()) {
|
||||
SessionDto sessionDto = sessionService.fetchUserSession(empNo);
|
||||
// Fetch session info via MCI using X-Praf-No (only when the service is available in a Tool Pod).
|
||||
if (sessionService != null && prafNo != null && !prafNo.isEmpty()) {
|
||||
SessionDto sessionDto = sessionService.fetchUserSession(prafNo);
|
||||
if (sessionDto != null) {
|
||||
// Set to request attribute so SessionUtil can find it
|
||||
request.setAttribute("userInfo", sessionDto);
|
||||
|
||||
@@ -2,9 +2,17 @@ package io.shinhanlife.dat.lib.mcp;
|
||||
|
||||
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
|
||||
public record McpRequestHeaders(
|
||||
String requestId,
|
||||
String guid,
|
||||
String mcpSessionId,
|
||||
String employeeNo,
|
||||
String virtualEmployeeNo) {
|
||||
String prafNo,
|
||||
String requestId,
|
||||
String requestTime,
|
||||
String vrtlPrafNo,
|
||||
String appCode,
|
||||
String projectCode,
|
||||
String userIp,
|
||||
String callerIp,
|
||||
String callerHost,
|
||||
String channel,
|
||||
String agentId,
|
||||
String mcpSessionId) {
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package io.shinhanlife.dat.lib.mcp;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
|
||||
import io.shinhanlife.dat.lib.util.ToolSchemaResolver;
|
||||
import io.shinhanlife.dat.lib.util.ToolSchemaResolver.ToolSchemaResourceException;
|
||||
import io.shinhanlife.dat.lib.validation.ToolArgumentSchemaValidator;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -27,7 +30,7 @@ public class McpToolExecutionService {
|
||||
String requestId = requestHeaders == null ? null : requestHeaders.requestId();
|
||||
String guid = requestHeaders == null ? null : requestHeaders.guid();
|
||||
String mcpSessionId = requestHeaders == null ? null : requestHeaders.mcpSessionId();
|
||||
log.info("[Tool] IN - guid: {}, x-request-id: {}, tool: {}", guid, requestId, functionName);
|
||||
log.info("[Tool] IN - X-Guid: {}, X-Request-Id: {}, tool: {}", guid, requestId, functionName);
|
||||
|
||||
McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName);
|
||||
if (resolvedTool == null) {
|
||||
@@ -44,12 +47,29 @@ public class McpToolExecutionService {
|
||||
return outputFailure;
|
||||
}
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
if (requestId != null) headers.put("x-request-id", requestId);
|
||||
if (guid != null) headers.put("guid", guid);
|
||||
if (requestId != null) headers.put("X-Request-Id", requestId);
|
||||
if (guid != null) headers.put("X-Guid", guid);
|
||||
if (mcpSessionId != null) headers.put("mcp-session-id", mcpSessionId);
|
||||
log.info("[Tool] OUT - guid: {}, x-request-id: {}, tool: {}", guid, requestId, functionName);
|
||||
log.info("[Tool] OUT - X-Guid: {}, X-Request-Id: {}, tool: {}", guid, requestId, functionName);
|
||||
return new ToolExecutionResult(200, methodResult, headers);
|
||||
} catch (Exception error) {
|
||||
} catch (IllegalArgumentException error) {
|
||||
log.error("[Tool] Tool argument conversion failed. tool={}", functionName, error);
|
||||
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
|
||||
} catch (IllegalAccessException error) {
|
||||
log.error("[Tool] Tool method is not accessible. tool={}", functionName, error);
|
||||
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
|
||||
} catch (InvocationTargetException error) {
|
||||
Throwable cause = error.getCause();
|
||||
if (cause instanceof BizException businessError) {
|
||||
log.error("[Tool] Tool business execution failed. tool={}", functionName, businessError);
|
||||
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
|
||||
}
|
||||
if (cause instanceof RuntimeException runtimeError) {
|
||||
throw runtimeError;
|
||||
}
|
||||
if (cause instanceof Error jvmError) {
|
||||
throw jvmError;
|
||||
}
|
||||
log.error("[Tool] Tool execution failed. tool={}", functionName, error);
|
||||
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
|
||||
}
|
||||
@@ -62,8 +82,8 @@ public class McpToolExecutionService {
|
||||
Map<String, Object> schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]);
|
||||
ValidationResponse result = toolArgumentSchemaValidator.validate(schema, arguments);
|
||||
return result.valid() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId);
|
||||
} catch (Exception error) {
|
||||
log.error("[Tool] Input schema validation failed unexpectedly. tool={}", tool.annotation().name(), error);
|
||||
} catch (ToolSchemaResourceException error) {
|
||||
log.error("[Tool] Input schema resource loading failed. tool={}", tool.annotation().name(), error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +93,8 @@ public class McpToolExecutionService {
|
||||
return objectMapper.convertValue(arguments, method.getParameterTypes()[0]);
|
||||
}
|
||||
|
||||
private Object invoke(McpToolMethodRegistry.RegisteredTool tool, Object argument) throws Exception {
|
||||
private Object invoke(McpToolMethodRegistry.RegisteredTool tool, Object argument)
|
||||
throws IllegalAccessException, InvocationTargetException {
|
||||
return tool.method().getParameterCount() == 0 ? tool.method().invoke(tool.bean()) : tool.method().invoke(tool.bean(), argument);
|
||||
}
|
||||
|
||||
@@ -84,8 +105,8 @@ public class McpToolExecutionService {
|
||||
if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).valid()) {
|
||||
return error(500, "INVALID_TOOL_RESPONSE", "Tool response does not match its output schema", requestId);
|
||||
}
|
||||
} catch (Exception error) {
|
||||
log.error("[Tool] Output schema validation failed unexpectedly. tool={}", tool.annotation().name(), error);
|
||||
} catch (ToolSchemaResourceException error) {
|
||||
log.error("[Tool] Output schema resource loading failed. tool={}", tool.annotation().name(), error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.dat.lib.session.dto.SessionDto;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -15,7 +15,7 @@ import java.util.List;
|
||||
@RequiredArgsConstructor
|
||||
public class AxhubSessionService {
|
||||
|
||||
private final AxhubMciComponent mciComponent;
|
||||
private final MciOnbszClient mciOnbszClient;
|
||||
|
||||
public SessionDto fetchUserSession(String employeeNo) {
|
||||
if (employeeNo == null || employeeNo.trim().isEmpty()) {
|
||||
@@ -23,18 +23,18 @@ public class AxhubSessionService {
|
||||
}
|
||||
|
||||
try {
|
||||
ONBSA0790_I request = ONBSA0790_I.builder()
|
||||
.plnrPrafScrnInqrInDto(ONBSA0790_I.PlnrPrafScrnInqrInDto.builder()
|
||||
ONBSZ0460_I.CmmnPrafIfinInDto input = ONBSZ0460_I.CmmnPrafIfinInDto.builder()
|
||||
.prafNo(employeeNo)
|
||||
.build())
|
||||
.build();
|
||||
ONBSZ0460_I request = ONBSZ0460_I.builder()
|
||||
.cmmnPrafIfinInDto(List.of(input))
|
||||
.build();
|
||||
|
||||
// callTo makes a synchronous MCI call.
|
||||
var responseTransfer = mciComponent.callTo("ONBSA0790", "ONBSA", request, ONBSA0790_O.class);
|
||||
ONBSA0790_O response = responseTransfer.getBody();
|
||||
ONBSZ0460_O response = mciOnbszClient.callOnbsz0460(request);
|
||||
|
||||
if (response != null && response.getPrafInfoDto() != null && !response.getPrafInfoDto().isEmpty()) {
|
||||
ONBSA0790_O.PrafInfoDto info = response.getPrafInfoDto().get(0);
|
||||
if (response != null && response.getCmmnPrafIfinOutDto() != null
|
||||
&& !response.getCmmnPrafIfinOutDto().isEmpty()) {
|
||||
ONBSZ0460_O.CmmnPrafIfinOutDto info = response.getCmmnPrafIfinOutDto().get(0);
|
||||
|
||||
SessionDto sessionDto = new SessionDto();
|
||||
sessionDto.setOgnzNo(info.getOgnzAsrtCd());
|
||||
@@ -48,11 +48,11 @@ public class AxhubSessionService {
|
||||
sessionDto.setOgnzAsrtCd(info.getOgnzAsrtCd());
|
||||
sessionDto.setOgnzLeveCd(info.getOgnzLeveCd());
|
||||
|
||||
log.info("[AxhubSessionService] MCI ONBSA0790 조회 성공: 사번={}, 조직코드={}", employeeNo, info.getOgnzAsrtCd());
|
||||
log.info("[AxhubSessionService] MCI ONBSZ0460 조회 성공: 사번={}, 조직코드={}", employeeNo, info.getOgnzAsrtCd());
|
||||
return sessionDto;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[AxhubSessionService] MCI ONBSA0790 사원 정보 조회 실패 (사번: {})", employeeNo, e);
|
||||
} catch (BizException e) {
|
||||
log.error("[AxhubSessionService] MCI ONBSZ0460 사원 정보 조회 실패 (사번: {})", employeeNo, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MciOnbszClient {
|
||||
|
||||
private static final String INTERFACE_ID = "ONBSZ0460";
|
||||
private static final String RECEIVE_SERVICE_ID = "ONBSZ";
|
||||
|
||||
private final AxhubMciComponent mciComponent;
|
||||
|
||||
public ONBSZ0460_O callOnbsz0460(ONBSZ0460_I request) {
|
||||
Transfer<ONBSZ0460_O> transfer = mciComponent.callTo(
|
||||
INTERFACE_ID, RECEIVE_SERVICE_ID, request, ONBSZ0460_O.class);
|
||||
return transfer == null ? null : transfer.getBody();
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
|
||||
import lombok.*;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ONBSA0790_I {
|
||||
|
||||
@GlowTrgmField(order = 1, length = 100, description = "설계사인사화면조회입력DTO")
|
||||
private PlnrPrafScrnInqrInDto plnrPrafScrnInqrInDto;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PlnrPrafScrnInqrInDto {
|
||||
@GlowTrgmField(order = 1, length = 7, description = "인사번호")
|
||||
private String prafNo;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
|
||||
import lombok.*;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ONBSA0790_O {
|
||||
|
||||
@GlowTrgmField(order = 1, length = 1000, description = "인사정보DTO리스트")
|
||||
private List<PrafInfoDto> prafInfoDto;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class PrafInfoDto {
|
||||
@GlowTrgmField(order = 1, length = 7, description = "인사번호")
|
||||
private String prafNo;
|
||||
|
||||
@GlowTrgmField(order = 2, length = 2, description = "인사유형코드")
|
||||
private String prafTypeCd;
|
||||
|
||||
@GlowTrgmField(order = 3, length = 100, description = "인사명")
|
||||
private String prafNm;
|
||||
|
||||
@GlowTrgmField(order = 22, length = 3, description = "영업직책코드")
|
||||
private String bsduCd;
|
||||
|
||||
@GlowTrgmField(order = 28, length = 6, description = "조직분류코드")
|
||||
private String ognzAsrtCd;
|
||||
|
||||
@GlowTrgmField(order = 29, length = 6, description = "인사조직분류코드")
|
||||
private String psmrAsrtCd;
|
||||
|
||||
@GlowTrgmField(order = 30, length = 6, description = "영업규정분류코드")
|
||||
private String sbsnRulpAsrtCd;
|
||||
|
||||
@GlowTrgmField(order = 31, length = 6, description = "조직레벨코드")
|
||||
private String ognzLeveCd;
|
||||
|
||||
@GlowTrgmField(order = 32, length = 7, description = "지점번호")
|
||||
private String brafNo;
|
||||
|
||||
@GlowTrgmField(order = 33, length = 7, description = "영업소코드")
|
||||
private String bsquCd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ONBSZ0460_I {
|
||||
|
||||
@GlowTrgmField(order = 1, description = "공통인사정보조회InDto", type = "gm")
|
||||
private List<CmmnPrafIfinInDto> cmmnPrafIfinInDto;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class CmmnPrafIfinInDto {
|
||||
|
||||
@GlowTrgmField(order = 1, length = 6, description = "마감년월")
|
||||
private String closYm;
|
||||
|
||||
@GlowTrgmField(order = 2, length = 7, description = "조직번호")
|
||||
private String ognzNo;
|
||||
|
||||
@GlowTrgmField(order = 3, length = 8, description = "인사번호")
|
||||
private String prafNo;
|
||||
|
||||
@GlowTrgmField(order = 4, length = 3, description = "인사조직분류코드")
|
||||
private String psmrAsrtCd;
|
||||
|
||||
@GlowTrgmField(order = 5, length = 1, description = "트레이너구분코드")
|
||||
private String trnrSccd;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ONBSZ0460_O {
|
||||
|
||||
@GlowTrgmField(order = 1, description = "공통인사정보조회OutDto", type = "gm")
|
||||
private List<CmmnPrafIfinOutDto> cmmnPrafIfinOutDto;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public static class CmmnPrafIfinOutDto {
|
||||
|
||||
@GlowTrgmField(order = 1, length = 8, description = "인사번호")
|
||||
private String prafNo;
|
||||
@GlowTrgmField(order = 2, length = 2, description = "인사유형코드")
|
||||
private String prafTypeCd;
|
||||
@GlowTrgmField(order = 3, length = 200, description = "인사명")
|
||||
private String prafNm;
|
||||
@GlowTrgmField(order = 4, length = 100, description = "인사영문명")
|
||||
private String prafEngNm;
|
||||
@GlowTrgmField(order = 5, length = 20, description = "생년월일")
|
||||
private String birymd;
|
||||
@GlowTrgmField(order = 6, length = 1, description = "성별코드")
|
||||
private String gndrCd;
|
||||
@GlowTrgmField(order = 7, length = 50, description = "주민등록번호")
|
||||
private String rdreNo;
|
||||
@GlowTrgmField(order = 8, length = 2, description = "조직분류코드")
|
||||
private String ognzAsrtCd;
|
||||
@GlowTrgmField(order = 9, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved09;
|
||||
@GlowTrgmField(order = 10, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved10;
|
||||
@GlowTrgmField(order = 11, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved11;
|
||||
@GlowTrgmField(order = 12, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved12;
|
||||
@GlowTrgmField(order = 13, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved13;
|
||||
@GlowTrgmField(order = 14, length = 0, description = "위촉구분코드 - 길이 확인 필요")
|
||||
private String appnSccd;
|
||||
@GlowTrgmField(order = 15, length = 2, description = "위촉경력구분코드")
|
||||
private String appnCareSccd;
|
||||
@GlowTrgmField(order = 16, length = 2, description = "위해촉최종상태코드")
|
||||
private String aprdLsstCd;
|
||||
@GlowTrgmField(order = 17, length = 20, description = "위촉일자")
|
||||
private String appnYmd;
|
||||
@GlowTrgmField(order = 18, length = 20, description = "부임일자")
|
||||
private String acsoYmd;
|
||||
@GlowTrgmField(order = 19, length = 20, description = "해임일자")
|
||||
private String dmssYmd;
|
||||
@GlowTrgmField(order = 20, length = 20, description = "해촉일자")
|
||||
private String dsapYmd;
|
||||
@GlowTrgmField(order = 21, length = 20, description = "재위촉일자")
|
||||
private String reapYmd;
|
||||
@GlowTrgmField(order = 22, length = 3, description = "영업직책코드")
|
||||
private String bsduCd;
|
||||
@GlowTrgmField(order = 23, length = 10, description = "생명보험협회등록번호")
|
||||
private String klirNo;
|
||||
@GlowTrgmField(order = 24, length = 2, description = "신인등급코드")
|
||||
private String nwfaGrdCd;
|
||||
@GlowTrgmField(order = 25, length = 2, description = "영업자격코드")
|
||||
private String bsquCd;
|
||||
@GlowTrgmField(order = 26, length = 6, description = "자격심사년월")
|
||||
private String qlfcIspaYm;
|
||||
@GlowTrgmField(order = 27, length = 2, description = "인사등급코드")
|
||||
private String prafGrdCd;
|
||||
@GlowTrgmField(order = 28, length = 0, description = "인사조직분류코드 - 길이 확인 필요")
|
||||
private String psmrAsrtCd;
|
||||
@GlowTrgmField(order = 29, length = 0, description = "영업규정분류코드 - 길이 확인 필요")
|
||||
private String sbsnRulpAsrtCd;
|
||||
@GlowTrgmField(order = 30, length = 0, description = "조직레벨코드 - 길이 확인 필요")
|
||||
private String ognzLeveCd;
|
||||
@GlowTrgmField(order = 31, length = 0, description = "지점번호 - 길이 확인 필요")
|
||||
private String brafNo;
|
||||
@GlowTrgmField(order = 32, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved32;
|
||||
@GlowTrgmField(order = 33, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved33;
|
||||
@GlowTrgmField(order = 34, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved34;
|
||||
@GlowTrgmField(order = 35, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved35;
|
||||
@GlowTrgmField(order = 36, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved36;
|
||||
@GlowTrgmField(order = 37, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved37;
|
||||
@GlowTrgmField(order = 38, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved38;
|
||||
@GlowTrgmField(order = 39, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved39;
|
||||
@GlowTrgmField(order = 40, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved40;
|
||||
@GlowTrgmField(order = 41, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved41;
|
||||
@GlowTrgmField(order = 42, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved42;
|
||||
@GlowTrgmField(order = 43, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved43;
|
||||
@GlowTrgmField(order = 44, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved44;
|
||||
@GlowTrgmField(order = 45, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved45;
|
||||
@GlowTrgmField(order = 46, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved46;
|
||||
@GlowTrgmField(order = 47, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved47;
|
||||
@GlowTrgmField(order = 48, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved48;
|
||||
@GlowTrgmField(order = 49, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved49;
|
||||
@GlowTrgmField(order = 50, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved50;
|
||||
@GlowTrgmField(order = 51, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved51;
|
||||
@GlowTrgmField(order = 52, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved52;
|
||||
@GlowTrgmField(order = 53, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved53;
|
||||
@GlowTrgmField(order = 54, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved54;
|
||||
@GlowTrgmField(order = 55, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved55;
|
||||
@GlowTrgmField(order = 56, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved56;
|
||||
@GlowTrgmField(order = 57, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved57;
|
||||
@GlowTrgmField(order = 58, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved58;
|
||||
@GlowTrgmField(order = 59, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved59;
|
||||
@GlowTrgmField(order = 60, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved60;
|
||||
@GlowTrgmField(order = 61, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved61;
|
||||
@GlowTrgmField(order = 62, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved62;
|
||||
@GlowTrgmField(order = 63, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved63;
|
||||
@GlowTrgmField(order = 64, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved64;
|
||||
@GlowTrgmField(order = 65, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved65;
|
||||
@GlowTrgmField(order = 66, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved66;
|
||||
@GlowTrgmField(order = 67, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved67;
|
||||
@GlowTrgmField(order = 68, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved68;
|
||||
@GlowTrgmField(order = 69, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved69;
|
||||
@GlowTrgmField(order = 70, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved70;
|
||||
@GlowTrgmField(order = 71, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved71;
|
||||
@GlowTrgmField(order = 72, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved72;
|
||||
@GlowTrgmField(order = 73, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved73;
|
||||
@GlowTrgmField(order = 74, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved74;
|
||||
@GlowTrgmField(order = 75, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved75;
|
||||
@GlowTrgmField(order = 76, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved76;
|
||||
@GlowTrgmField(order = 77, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved77;
|
||||
@GlowTrgmField(order = 78, length = 0, description = "TODO 전문 명세 매핑")
|
||||
private String reserved78;
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,6 @@ public class PodScaffolder {
|
||||
public static void main(String[] args) throws IOException {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
|
||||
System.out.println("=========================================");
|
||||
System.out.println(" MCP Tool Pod Scaffolder (Java CLI) ");
|
||||
System.out.println("=========================================\n");
|
||||
|
||||
String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dat-was-payment): ");
|
||||
String moduleName = rawModuleName.startsWith("dat-was-") ? rawModuleName : "dat-was-" + rawModuleName;
|
||||
String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): ");
|
||||
@@ -33,14 +29,12 @@ public class PodScaffolder {
|
||||
if (createDate.trim().isEmpty()) createDate = defaultDate;
|
||||
|
||||
String result = scaffoldPod(moduleName, portStr, shortName, author, createDate);
|
||||
System.out.println(result);
|
||||
}
|
||||
|
||||
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
|
||||
if (args.length > index) {
|
||||
return args[index];
|
||||
}
|
||||
System.out.print(prompt);
|
||||
return scanner.nextLine().trim();
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,9 @@ public class ToolScaffolder {
|
||||
writeStructuredFieldTypes(ioDir, ioPackage + ".io", tool.interfaceId() + "_O", tool.outputFields());
|
||||
String sysCode = tool.clientSystemCode();
|
||||
if (sysCode != null && sysCode.length() == 4) {
|
||||
String clientCap = toPascalCase(sysCode);
|
||||
String normalizedSysCode = sysCode.toLowerCase(Locale.ROOT);
|
||||
String clientCap = normalizedSysCode.substring(0, 1).toUpperCase(Locale.ROOT)
|
||||
+ normalizedSysCode.substring(1);
|
||||
writeUtf8(clientDir.resolve("Mci" + clientCap + "Client.java"),
|
||||
"package " + ioPackage + ";\n\nimport io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;\nimport io.shinhanlife.glow.communication.dto.Transfer;\nimport lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\n\n@Component\n@RequiredArgsConstructor\npublic class Mci" + clientCap + "Client {\n private final AxhubMciComponent mci;\n\n public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> resType) throws Exception {\n return mci.callTo(interfaceId, dummy, mciReq, resType);\n }\n}\n");
|
||||
} else {
|
||||
@@ -615,6 +617,9 @@ public class ToolScaffolder {
|
||||
title = title == null || title.isBlank() ? baseName : title.trim();
|
||||
description = description == null ? "" : description.trim();
|
||||
httpApiName = httpApiName == null || httpApiName.isBlank() ? toKebabCase(baseName) : httpApiName.trim();
|
||||
if (definitionOptions == null) {
|
||||
definitionOptions = new ToolDefinitionOptions(null, null, null, null, null, null, null, null);
|
||||
}
|
||||
String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR");
|
||||
if (envSourceDir == null) {
|
||||
envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.annotation.McpOutputSchema;
|
||||
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@@ -66,13 +67,25 @@ public class ToolSchemaResolver {
|
||||
: location;
|
||||
ClassPathResource resource = new ClassPathResource(path);
|
||||
if (!resource.exists()) {
|
||||
throw new IllegalStateException("MCP schema resource not found: " + location);
|
||||
throw new ToolSchemaResourceException("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 schema resource: " + location, e);
|
||||
} catch (IOException e) {
|
||||
throw new ToolSchemaResourceException("Failed to load MCP schema resource: " + location, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ToolSchemaResourceException extends IllegalStateException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ToolSchemaResourceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ToolSchemaResourceException(String message, IOException cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,6 @@ public final class McpToolNameValidationRunner {
|
||||
private McpToolNameValidationRunner() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 1) {
|
||||
throw new IllegalArgumentException("Usage: McpToolNameValidationRunner <project-root>");
|
||||
}
|
||||
validate(Path.of(args[0]));
|
||||
}
|
||||
|
||||
static void validate(Path projectRoot) {
|
||||
McpToolNameValidator.assertUnique(projectRoot);
|
||||
}
|
||||
|
||||
@@ -34,15 +34,24 @@ public class BusinessToolController {
|
||||
@PostMapping("/mcp/{name}")
|
||||
public ResponseEntity<?> executeDynamicTool(
|
||||
@PathVariable("name") String functionName,
|
||||
@RequestHeader(value = "x-request-id", required = false) String requestId,
|
||||
@RequestHeader(value = "guid", required = false) String guid,
|
||||
@RequestHeader(value = "X-Guid", required = false) String guid,
|
||||
@RequestHeader(value = "X-Praf-No", required = false) String prafNo,
|
||||
@RequestHeader(value = "X-Request-Id", required = false) String requestId,
|
||||
@RequestHeader(value = "X-Request-Time", required = false) String requestTime,
|
||||
@RequestHeader(value = "X-Vrtl-Praf-No", required = false) String vrtlPrafNo,
|
||||
@RequestHeader(value = "X-App-Code", required = false) String appCode,
|
||||
@RequestHeader(value = "X-Project-Code", required = false) String projectCode,
|
||||
@RequestHeader(value = "X-User-Ip", required = false) String userIp,
|
||||
@RequestHeader(value = "X-Caller-Ip", required = false) String callerIp,
|
||||
@RequestHeader(value = "X-Caller-Host", required = false) String callerHost,
|
||||
@RequestHeader(value = "X-Channel", required = false) String channel,
|
||||
@RequestHeader(value = "X-Agent-Id", required = false) String agentId,
|
||||
@RequestHeader(value = "mcp-session-id", required = false) String mcpSessionId,
|
||||
@RequestHeader(value = "employee-no", required = false) String employeeNo,
|
||||
@RequestHeader(value = "virtual-employee-no", required = false) String virtualEmployeeNo,
|
||||
@RequestBody(required = false) Map<String, Object> arguments) {
|
||||
ToolExecutionResult result = toolExecutionService.execute(
|
||||
functionName,
|
||||
new McpRequestHeaders(requestId, guid, mcpSessionId, employeeNo, virtualEmployeeNo),
|
||||
new McpRequestHeaders(guid, prafNo, requestId, requestTime, vrtlPrafNo, appCode,
|
||||
projectCode, userIp, callerIp, callerHost, channel, agentId, mcpSessionId),
|
||||
arguments);
|
||||
ResponseEntity.BodyBuilder response = ResponseEntity.status(result.statusCode());
|
||||
result.headers().forEach(response::header);
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</aside>
|
||||
<section class="stack">
|
||||
<section class="card"><h2>2. 요청 JSON</h2><div class="notice">필수값과 형식은 Tool의 inputSchema 기준입니다. MCI·외부 연동 Tool은 업무에 맞는 테스트 데이터를 입력한 후 저장하세요.</div><textarea id="arguments" spellcheck="false" aria-label="요청 JSON"></textarea><div class="buttons" style="margin-top:12px"><button id="saveButton">현재 요청 저장</button><button class="primary" id="executeButton">실행</button></div></section>
|
||||
<section class="card"><h2>3. 실행 결과</h2><div class="meta"><span class="badge" id="httpStatus">대기</span><span class="badge" id="latency">-</span><span class="badge" id="traceId">guid: -</span><span class="badge" id="requestId">x-request-id: -</span></div><pre class="result" id="result">Tool을 선택하고 실행하세요.</pre></section>
|
||||
<section class="card"><h2>3. 실행 결과</h2><div class="meta"><span class="badge" id="httpStatus">대기</span><span class="badge" id="latency">-</span><span class="badge" id="traceId">X-Guid: -</span><span class="badge" id="requestId">X-Request-Id: -</span></div><pre class="result" id="result">Tool을 선택하고 실행하세요.</pre></section>
|
||||
</section>
|
||||
</div>
|
||||
<p class="footer-note">이 화면은 현재 Tool Pod의 <code>/tool-manifest</code>와 <code>/mcp/{toolName}</code>만 사용합니다. 저장된 케이스는 이 브라우저의 localStorage에만 보관됩니다.</p>
|
||||
@@ -257,7 +257,25 @@
|
||||
const fallback = `/mcp/${encodeURIComponent(tool.name)}`;
|
||||
try { const endpoint = new URL(tool.endpoint || fallback, window.location.origin); return endpoint.origin === window.location.origin ? `${endpoint.pathname}${endpoint.search}` : fallback; } catch (_) { return fallback; }
|
||||
}
|
||||
function resetResult() { $('httpStatus').textContent = '대기'; $('httpStatus').className = 'badge'; $('latency').textContent = '-'; $('traceId').textContent = 'guid: -'; $('requestId').textContent = 'x-request-id: -'; }
|
||||
function resetResult() { $('httpStatus').textContent = '대기'; $('httpStatus').className = 'badge'; $('latency').textContent = '-'; $('traceId').textContent = 'X-Guid: -'; $('requestId').textContent = 'X-Request-Id: -'; }
|
||||
function toolHeaders(guid, request, session) {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Guid': guid,
|
||||
'X-Praf-No': '100001',
|
||||
'X-Request-Id': request,
|
||||
'X-Request-Time': new Date().toISOString(),
|
||||
'X-Vrtl-Praf-No': 'V100001',
|
||||
'X-App-Code': 'DATMT',
|
||||
'X-Project-Code': 'AXHUB',
|
||||
'X-User-Ip': '127.0.0.1',
|
||||
'X-Caller-Ip': '127.0.0.1',
|
||||
'X-Caller-Host': window.location.hostname || 'localhost',
|
||||
'X-Channel': 'TOOL-TEST-CONSOLE',
|
||||
'X-Agent-Id': 'TOOL-TEST-CONSOLE',
|
||||
'mcp-session-id': session
|
||||
};
|
||||
}
|
||||
|
||||
async function execute(tool = state.selected, body = null) {
|
||||
if (!tool) throw new Error('실행할 Tool을 선택하세요.');
|
||||
@@ -269,13 +287,13 @@
|
||||
const reqPayload = { jsonrpc: "2.0", method: "tools/call", params: { name: tool.name, arguments: payload }, id: Date.now() };
|
||||
response = await fetch('/mcp/api/v1/tools/call', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'guid':guid, 'x-request-id':request, 'mcp-session-id':session },
|
||||
headers: toolHeaders(guid, request, session),
|
||||
body: JSON.stringify(reqPayload)
|
||||
});
|
||||
} else {
|
||||
response = await fetch(endpointFor(tool), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'guid':guid, 'x-request-id':request, 'mcp-session-id':session },
|
||||
headers: toolHeaders(guid, request, session),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
@@ -290,7 +308,7 @@
|
||||
|
||||
const elapsed = Math.round(performance.now() - started);
|
||||
$('httpStatus').textContent = `HTTP ${displayStatus}`; $('httpStatus').className = `badge ${isOk ? 'ok' : 'fail'}`;
|
||||
$('latency').textContent = `${elapsed}ms`; $('traceId').textContent = `guid: ${response.headers.get('guid') || guid}`; $('requestId').textContent = `x-request-id: ${response.headers.get('x-request-id') || request}`;
|
||||
$('latency').textContent = `${elapsed}ms`; $('traceId').textContent = `X-Guid: ${response.headers.get('X-Guid') || guid}`; $('requestId').textContent = `X-Request-Id: ${response.headers.get('X-Request-Id') || request}`;
|
||||
let displayData = data;
|
||||
if (isGatewayMode && data && typeof data === 'object') {
|
||||
if (data.result && data.result.result) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package io.shinhanlife.dat.lib.adapter.test;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class MockEimsHttpServerTest {
|
||||
|
||||
@@ -16,4 +20,28 @@ class MockEimsHttpServerTest {
|
||||
assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
|
||||
assertThat(response.getBody().path("resultCode").asText()).isEqualTo("SUCCESS");
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsInternalServerErrorForMalformedMockJson() {
|
||||
MockEimsHttpServer server = new MockEimsHttpServer(new ObjectMapper());
|
||||
|
||||
var response = server.mockToolHttpResponse("invalid_mock_response", null);
|
||||
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(500);
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotSwallowUnexpectedRuntimeExceptions() {
|
||||
ObjectMapper failingMapper = new ObjectMapper() {
|
||||
@Override
|
||||
public JsonNode readTree(InputStream input) throws IOException {
|
||||
throw new IllegalStateException("unexpected mapper failure");
|
||||
}
|
||||
};
|
||||
MockEimsHttpServer server = new MockEimsHttpServer(failingMapper);
|
||||
|
||||
assertThatThrownBy(() -> server.mockToolHttpResponse("cmm_memo_retriever", null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessage("unexpected mapper failure");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.shinhanlife.dat.lib.aop;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.shinhanlife.dat.lib.config.McpProperties;
|
||||
import java.lang.reflect.Method;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
|
||||
class ToolSlaMonitoringAspectTest {
|
||||
|
||||
@Test
|
||||
void propagatesJvmErrorsWithoutInspectingThem() throws Throwable {
|
||||
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
|
||||
MethodSignature signature = mock(MethodSignature.class);
|
||||
Method method = SampleTool.class.getDeclaredMethod("execute");
|
||||
MessageAccessError error = new MessageAccessError();
|
||||
when(joinPoint.getSignature()).thenReturn(signature);
|
||||
when(signature.getMethod()).thenReturn(method);
|
||||
when(joinPoint.proceed()).thenThrow(error);
|
||||
|
||||
MessageAccessError thrown = assertThrows(MessageAccessError.class,
|
||||
() -> new ToolSlaMonitoringAspect(new McpProperties()).monitorToolSla(joinPoint));
|
||||
|
||||
assertSame(error, thrown);
|
||||
}
|
||||
|
||||
static class SampleTool {
|
||||
@McpTool(name = "sample_tool")
|
||||
public void execute() {
|
||||
}
|
||||
}
|
||||
|
||||
static class MessageAccessError extends Error {
|
||||
@Override
|
||||
public String getMessage() {
|
||||
throw new AssertionError("JVM errors must not be inspected by the SLA aspect");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class AxhubHttpComponentTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardsDapmsHeadersToTheConfiguredHttpService() throws Exception {
|
||||
void forwardsCanonicalToolHeadersToTheConfiguredHttpService() throws Exception {
|
||||
RestClient.Builder builder = RestClient.builder();
|
||||
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
|
||||
AxhubHttpProperties properties = new AxhubHttpProperties();
|
||||
@@ -79,22 +79,35 @@ class AxhubHttpComponentTest {
|
||||
new GlowHttpComponent(builder), new ObjectMapper(), new GlowCommunicationProperties(), properties);
|
||||
|
||||
server.expect(requestTo("https://api.example.test/v1"))
|
||||
.andExpect(header("x-request-id", "request-1"))
|
||||
.andExpect(header("guid", "guid-1"))
|
||||
.andExpect(header("X-Guid", "guid-1"))
|
||||
.andExpect(header("X-Praf-No", "praf-1"))
|
||||
.andExpect(header("X-Request-Id", "request-1"))
|
||||
.andExpect(header("X-Request-Time", "2026-08-25T12:34:56+09:00"))
|
||||
.andExpect(header("X-Vrtl-Praf-No", "virtual-1"))
|
||||
.andExpect(header("X-App-Code", "app-1"))
|
||||
.andExpect(header("X-Project-Code", "project-1"))
|
||||
.andExpect(header("X-User-Ip", "10.0.0.1"))
|
||||
.andExpect(header("X-Caller-Ip", "10.0.0.2"))
|
||||
.andExpect(header("X-Caller-Host", "caller.example.test"))
|
||||
.andExpect(header("X-Channel", "MCP"))
|
||||
.andExpect(header("X-Agent-Id", "agent-1"))
|
||||
.andExpect(header("mcp-session-id", "session-1"))
|
||||
.andExpect(header("employee-no", "ENC(employee)"))
|
||||
.andExpect(header("virtual-employee-no", "ENC(virtual)"))
|
||||
.andRespond(withSuccess("{\"status\":\"OK\"}", APPLICATION_JSON));
|
||||
|
||||
setRequestHeaders(headers(Map.of(
|
||||
"requestId", "request-1",
|
||||
"guid", "guid-1",
|
||||
"mcpSessionId", "session-1",
|
||||
"employeeNo", "ENC(employee)",
|
||||
"virtualEmployeeNo", "ENC(virtual)",
|
||||
"headerRequestId", "request-1",
|
||||
"traceId", "guid-1",
|
||||
"encryptedEmployeeId", "ENC(employee)")));
|
||||
setRequestHeaders(headers(Map.ofEntries(
|
||||
Map.entry("guid", "guid-1"),
|
||||
Map.entry("prafNo", "praf-1"),
|
||||
Map.entry("requestId", "request-1"),
|
||||
Map.entry("requestTime", "2026-08-25T12:34:56+09:00"),
|
||||
Map.entry("vrtlPrafNo", "virtual-1"),
|
||||
Map.entry("appCode", "app-1"),
|
||||
Map.entry("projectCode", "project-1"),
|
||||
Map.entry("userIp", "10.0.0.1"),
|
||||
Map.entry("callerIp", "10.0.0.2"),
|
||||
Map.entry("callerHost", "caller.example.test"),
|
||||
Map.entry("channel", "MCP"),
|
||||
Map.entry("agentId", "agent-1"),
|
||||
Map.entry("mcpSessionId", "session-1"))));
|
||||
try {
|
||||
assertThat(component.call("status", Map.of(), SampleResponse.class).status()).isEqualTo("OK");
|
||||
server.verify();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.shinhanlife.dat.lib.integration.mci.component;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.shinhanlife.dat.lib.config.GlowCommunicationProperties;
|
||||
import io.shinhanlife.glow.communication.module.mci.component.GlowMciComponent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AxhubMciComponentTest {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void doesNotReclassifyUnexpectedRuntimeFailuresAsBusinessFailures() {
|
||||
GlowMciComponent glowMci = mock(GlowMciComponent.class);
|
||||
IllegalStateException failure = new IllegalStateException("unexpected MCI adapter defect");
|
||||
when(glowMci.sync(any())).thenThrow(failure);
|
||||
AxhubMciComponent component = new AxhubMciComponent(glowMci, new GlowCommunicationProperties());
|
||||
|
||||
IllegalStateException thrown = assertThrows(IllegalStateException.class, () ->
|
||||
component.callTo("ONBSA0790", "ONBSA", new Object(), Object.class));
|
||||
|
||||
assertSame(failure, thrown);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
package io.shinhanlife.dat.lib.mcp;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||
@@ -29,23 +34,72 @@ class McpToolExecutionServiceTest {
|
||||
void convertsArgumentsToDtoAndExecutesTheMatchedTool() {
|
||||
McpToolExecutionService service = serviceWith(new EchoTool());
|
||||
ToolExecutionResult result = service.execute("sample.cmm.value.echo",
|
||||
headers(Map.of(
|
||||
"requestId", "request-1",
|
||||
"guid", "guid-1",
|
||||
"mcpSessionId", "session-1",
|
||||
"employeeNo", "employee-1",
|
||||
"virtualEmployeeNo", "virtual-1",
|
||||
"headerRequestId", "request-1",
|
||||
"traceId", "guid-1",
|
||||
"encryptedEmployeeId", "employee-1")),
|
||||
headers(Map.ofEntries(
|
||||
Map.entry("guid", "guid-1"),
|
||||
Map.entry("prafNo", "praf-1"),
|
||||
Map.entry("requestId", "request-1"),
|
||||
Map.entry("requestTime", "2026-08-25T12:34:56+09:00"),
|
||||
Map.entry("vrtlPrafNo", "virtual-1"),
|
||||
Map.entry("appCode", "app-1"),
|
||||
Map.entry("projectCode", "project-1"),
|
||||
Map.entry("userIp", "10.0.0.1"),
|
||||
Map.entry("callerIp", "10.0.0.2"),
|
||||
Map.entry("callerHost", "caller.example.test"),
|
||||
Map.entry("channel", "MCP"),
|
||||
Map.entry("agentId", "agent-1"),
|
||||
Map.entry("mcpSessionId", "session-1"))),
|
||||
Map.of("value", "hello"));
|
||||
assertEquals(200, result.statusCode());
|
||||
assertEquals("hello", ((Map<?, ?>) result.body()).get("value"));
|
||||
assertEquals("request-1", result.headers().get("x-request-id"));
|
||||
assertEquals("guid-1", result.headers().get("guid"));
|
||||
assertEquals("request-1", result.headers().get("X-Request-Id"));
|
||||
assertEquals("guid-1", result.headers().get("X-Guid"));
|
||||
assertEquals("session-1", result.headers().get("mcp-session-id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHideUnexpectedInputSchemaResolverFailures() {
|
||||
ToolSchemaResolver resolver = mock(ToolSchemaResolver.class);
|
||||
when(resolver.resolve(any(), any(), any()))
|
||||
.thenThrow(new IllegalStateException("unexpected resolver defect"));
|
||||
McpToolExecutionService service = serviceWith(new EchoTool(), resolver);
|
||||
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
service.execute("sample.cmm.value.echo", null, Map.of("value", "hello")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHideUnexpectedOutputSchemaResolverFailures() {
|
||||
ToolSchemaResolver resolver = mock(ToolSchemaResolver.class);
|
||||
when(resolver.resolveOutput(any(), any(), any()))
|
||||
.thenThrow(new IllegalStateException("unexpected resolver defect"));
|
||||
McpToolExecutionService service = serviceWith(new MapTool(), resolver);
|
||||
|
||||
assertThrows(IllegalStateException.class, () ->
|
||||
service.execute("sample.cmm.map.echo", null, Map.of("value", "hello")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesUnexpectedRuntimeFailuresThrownByTheTool() {
|
||||
UnexpectedToolFailure failure = new UnexpectedToolFailure();
|
||||
McpToolExecutionService service = serviceWith(new RuntimeFailureTool(failure));
|
||||
|
||||
UnexpectedToolFailure thrown = assertThrows(UnexpectedToolFailure.class, () ->
|
||||
service.execute("sample.cmm.runtime.failure", null, Map.of()));
|
||||
|
||||
assertSame(failure, thrown);
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesJvmErrorsThrownByTheTool() {
|
||||
ToolJvmError error = new ToolJvmError();
|
||||
McpToolExecutionService service = serviceWith(new ErrorTool(error));
|
||||
|
||||
ToolJvmError thrown = assertThrows(ToolJvmError.class, () ->
|
||||
service.execute("sample.cmm.error.failure", null, Map.of()));
|
||||
|
||||
assertSame(error, thrown);
|
||||
}
|
||||
|
||||
private McpRequestHeaders headers(Map<String, String> values) {
|
||||
try {
|
||||
Class<?>[] types = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||
@@ -61,6 +115,11 @@ class McpToolExecutionServiceTest {
|
||||
}
|
||||
|
||||
private McpToolExecutionService serviceWith(Object toolBean) {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
return serviceWith(toolBean, new ToolSchemaResolver(objectMapper));
|
||||
}
|
||||
|
||||
private McpToolExecutionService serviceWith(Object toolBean, ToolSchemaResolver resolver) {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.getBeanFactory().registerSingleton("toolBean", toolBean);
|
||||
@@ -69,7 +128,7 @@ class McpToolExecutionServiceTest {
|
||||
registry.initialize();
|
||||
return new McpToolExecutionService(registry, objectMapper,
|
||||
new ToolArgumentSchemaValidator(objectMapper, new DefaultJsonSchemaValidator()),
|
||||
new ToolSchemaResolver(objectMapper));
|
||||
resolver);
|
||||
}
|
||||
|
||||
static class EchoTool {
|
||||
@@ -80,6 +139,45 @@ class McpToolExecutionServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
static class MapTool {
|
||||
@McpTool(name = "sample.cmm.map.echo", description = "Echoes a map")
|
||||
public Map<String, Object> execute(Map<String, Object> request) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
static class RuntimeFailureTool {
|
||||
private final UnexpectedToolFailure failure;
|
||||
|
||||
RuntimeFailureTool(UnexpectedToolFailure failure) {
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@McpTool(name = "sample.cmm.runtime.failure")
|
||||
public Map<String, Object> execute(Map<String, Object> request) {
|
||||
throw failure;
|
||||
}
|
||||
}
|
||||
|
||||
static class ErrorTool {
|
||||
private final ToolJvmError error;
|
||||
|
||||
ErrorTool(ToolJvmError error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
@McpTool(name = "sample.cmm.error.failure")
|
||||
public Map<String, Object> execute(Map<String, Object> request) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static class UnexpectedToolFailure extends RuntimeException {
|
||||
}
|
||||
|
||||
static class ToolJvmError extends Error {
|
||||
}
|
||||
|
||||
static class EchoRequest {
|
||||
@McpToolParam(description = "Value", required = true)
|
||||
private String value;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package io.shinhanlife.dat.lib.session.mci;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.dat.lib.session.dto.SessionDto;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
class AxhubSessionServiceTest {
|
||||
|
||||
private static final String PACKAGE = "io.shinhanlife.dat.lib.session.mci.";
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void fetchesSessionThroughOnbsz0460ListContract() throws Exception {
|
||||
Class<?> requestClass = requiredClass("ONBSZ0460_I");
|
||||
Class<?> responseClass = requiredClass("ONBSZ0460_O");
|
||||
Class<?> outputClass = requiredClass("ONBSZ0460_O$CmmnPrafIfinOutDto");
|
||||
Class<?> clientClass = requiredClass("MciOnbszClient");
|
||||
|
||||
Object output = outputClass.getDeclaredConstructor().newInstance();
|
||||
set(outputClass, output, "PrafNo", "1234567");
|
||||
set(outputClass, output, "OgnzAsrtCd", "ORG001");
|
||||
set(outputClass, output, "BrafNo", "BR001");
|
||||
set(outputClass, output, "PsmrAsrtCd", "PSMR01");
|
||||
set(outputClass, output, "SbsnRulpAsrtCd", "RULE01");
|
||||
set(outputClass, output, "BsduCd", "BSD");
|
||||
set(outputClass, output, "BsquCd", "BQ");
|
||||
set(outputClass, output, "OgnzLeveCd", "LEVEL1");
|
||||
|
||||
Object response = responseClass.getDeclaredConstructor().newInstance();
|
||||
responseClass.getMethod("setCmmnPrafIfinOutDto", List.class).invoke(response, List.of(output));
|
||||
Transfer transfer = mock(Transfer.class);
|
||||
when(transfer.getBody()).thenReturn(response);
|
||||
AxhubMciComponent mci = mock(AxhubMciComponent.class);
|
||||
when(mci.callTo(eq("ONBSZ0460"), eq("ONBSZ"), any(), eq((Class) responseClass)))
|
||||
.thenReturn(transfer);
|
||||
|
||||
Object client = clientClass.getDeclaredConstructor(AxhubMciComponent.class).newInstance(mci);
|
||||
AxhubSessionService service = AxhubSessionService.class.getDeclaredConstructor(clientClass).newInstance(client);
|
||||
|
||||
SessionDto session = service.fetchUserSession("1234567");
|
||||
|
||||
assertEquals("1234567", session.getPrafNo());
|
||||
assertEquals("ORG001", session.getOgnzNo());
|
||||
assertEquals("BR001", session.getBrafNo());
|
||||
ArgumentCaptor<Object> requestCaptor = ArgumentCaptor.forClass(Object.class);
|
||||
verify(mci).callTo(eq("ONBSZ0460"), eq("ONBSZ"), requestCaptor.capture(), eq((Class) responseClass));
|
||||
Object request = requestCaptor.getValue();
|
||||
assertEquals(requestClass, request.getClass());
|
||||
List<?> inputs = (List<?>) requestClass.getMethod("getCmmnPrafIfinInDto").invoke(request);
|
||||
assertEquals(1, inputs.size());
|
||||
assertEquals("1234567", inputs.get(0).getClass().getMethod("getPrafNo").invoke(inputs.get(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void exposesSeventyEightOutputMappingSlots() {
|
||||
Class<?> outputClass = requiredClass("ONBSZ0460_O$CmmnPrafIfinOutDto");
|
||||
Field[] fields = outputClass.getDeclaredFields();
|
||||
|
||||
assertEquals(78, fields.length);
|
||||
List<Integer> actualOrders = Arrays.stream(fields)
|
||||
.map(field -> field.getAnnotation(GlowTrgmField.class))
|
||||
.map(GlowTrgmField::order)
|
||||
.sorted()
|
||||
.toList();
|
||||
assertEquals(IntStream.rangeClosed(1, 78).boxed().toList(), actualOrders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removesOldOnbsa0790Types() {
|
||||
assertThrows(ClassNotFoundException.class, () -> Class.forName(PACKAGE + "ONBSA0790_I"));
|
||||
assertThrows(ClassNotFoundException.class, () -> Class.forName(PACKAGE + "ONBSA0790_O"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void returnsNullWhenMciReportsABusinessCommunicationFailure() throws Exception {
|
||||
Class<?> responseClass = requiredClass("ONBSZ0460_O");
|
||||
Class<?> clientClass = requiredClass("MciOnbszClient");
|
||||
AxhubMciComponent mci = mock(AxhubMciComponent.class);
|
||||
when(mci.callTo(eq("ONBSZ0460"), eq("ONBSZ"), any(), eq((Class) responseClass)))
|
||||
.thenThrow(bizException());
|
||||
Object client = clientClass.getDeclaredConstructor(AxhubMciComponent.class).newInstance(mci);
|
||||
AxhubSessionService service = AxhubSessionService.class.getDeclaredConstructor(clientClass).newInstance(client);
|
||||
|
||||
assertNull(service.fetchUserSession("1234567"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
void doesNotHideUnexpectedRuntimeFailures() throws Exception {
|
||||
Class<?> responseClass = requiredClass("ONBSZ0460_O");
|
||||
Class<?> clientClass = requiredClass("MciOnbszClient");
|
||||
AxhubMciComponent mci = mock(AxhubMciComponent.class);
|
||||
when(mci.callTo(eq("ONBSZ0460"), eq("ONBSZ"), any(), eq((Class) responseClass)))
|
||||
.thenThrow(new IllegalStateException("unexpected defect"));
|
||||
Object client = clientClass.getDeclaredConstructor(AxhubMciComponent.class).newInstance(mci);
|
||||
AxhubSessionService service = AxhubSessionService.class.getDeclaredConstructor(clientClass).newInstance(client);
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> service.fetchUserSession("1234567"));
|
||||
}
|
||||
|
||||
private Class<?> requiredClass(String simpleName) {
|
||||
return assertDoesNotThrow(() -> Class.forName(PACKAGE + simpleName));
|
||||
}
|
||||
|
||||
private void set(Class<?> type, Object target, String property, String value) throws Exception {
|
||||
Method setter = type.getMethod("set" + property, String.class);
|
||||
setter.invoke(target, value);
|
||||
}
|
||||
|
||||
private BizException bizException() {
|
||||
return new BizException("CST00477", new String[]{"MCI communication failure"},
|
||||
new RuntimeException("connection closed"));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dat.lib.metadata.ToolDefinitionValidator;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -42,13 +38,13 @@ class ToolScaffolderTest {
|
||||
Path sourceRoot = root.resolve("dat-was-customer/src/main/java/io/shinhanlife/dat/mcc");
|
||||
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
|
||||
String implementation = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
|
||||
String guidanceClient = Files.readString(sourceRoot.resolve("infra/itrf/mci/nild/CustomerGuidanceClient.java"));
|
||||
String guidanceClient = Files.readString(sourceRoot.resolve("infra/itrf/mci/nil/d/MciNildClient.java"));
|
||||
|
||||
assertTrue(useCase.contains("CustomerGuidanceResponse searchGuidance(CustomerGuidanceRequest req)"), useCase);
|
||||
assertTrue(useCase.contains("CustomerContractResponse searchContract(CustomerContractRequest req)"), useCase);
|
||||
assertTrue(implementation.contains("private final CustomerGuidanceClient customerGuidanceClient;"), implementation);
|
||||
assertTrue(implementation.contains("customerGuidanceClient.callCustomerGuidance(request)"), implementation);
|
||||
assertTrue(guidanceClient.contains("CustomerGuidance_O callCustomerGuidance(CustomerGuidance_I request)"), guidanceClient);
|
||||
assertTrue(implementation.contains("private final MciNildClient mciNildClient;"), implementation);
|
||||
assertTrue(implementation.contains("mciNildClient.callTo(\"CTMNILO00007\", null, request, CTMNILO00007_O.class)"), implementation);
|
||||
assertTrue(guidanceClient.contains("class MciNildClient"), guidanceClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,7 +81,6 @@ class ToolScaffolderTest {
|
||||
Path dtoRoot = root.resolve("dat-was-claim/src/main/java/io/shinhanlife/dat/mcc/biz/cmm/dto");
|
||||
String request = Files.readString(dtoRoot.resolve("ClaimSearchRequest.java"));
|
||||
String response = Files.readString(dtoRoot.resolve("ClaimSearchResponse.java"));
|
||||
String definition = Files.readString(root.resolve("dat-was-claim/src/main/resources/tool-definitions/cmm/cmm_claim_search.yml"));
|
||||
String mock = Files.readString(root.resolve("dat-was-claim/src/main/resources/mock-responses/cmm_claim_search.json"));
|
||||
|
||||
assertTrue(request.contains("private ClaimStatus claimStatus;"), request);
|
||||
@@ -94,8 +89,6 @@ class ToolScaffolderTest {
|
||||
assertTrue(response.contains("private List<GuidanceItemsItem> guidanceItems;"), response);
|
||||
assertTrue(response.contains("public static class GuidanceItemsItem"), response);
|
||||
assertFalse(Files.exists(dtoRoot.resolve("ClaimSearchResponseGuidanceItemsItem.java")));
|
||||
assertTrue(definition.contains("enum: [OPEN, CLOSED]"), definition);
|
||||
assertTrue(definition.contains("type: array"), definition);
|
||||
assertTrue(mock.contains("\"guidanceItems\" : [{"), mock);
|
||||
}
|
||||
|
||||
@@ -140,7 +133,7 @@ class ToolScaffolderTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesV17ToolDefinitionTogetherWithToolSources() throws Exception {
|
||||
void generatesGrowToolHintMetadataTogetherWithToolSources() throws Exception {
|
||||
String moduleName = root.resolve("dat-was-v17-definition").toString();
|
||||
|
||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회",
|
||||
@@ -149,16 +142,13 @@ class ToolScaffolderTest {
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", List.of("09860000"), "", true)),
|
||||
List.of(), "employee");
|
||||
|
||||
Path definition = root.resolve("dat-was-v17-definition/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||
String yaml = Files.readString(definition);
|
||||
Path useCasePath = root.resolve("dat-was-v17-definition/src/main/java/io/shinhanlife/dat/mcc/biz/smp/usecase/EmployeeSearchUseCase.java");
|
||||
String useCase = Files.readString(useCasePath);
|
||||
|
||||
assertTrue(yaml.contains("name: smp_employee_search"), yaml);
|
||||
assertTrue(yaml.contains("when_to_use:"), yaml);
|
||||
assertTrue(yaml.contains("example_queries:"), yaml);
|
||||
assertTrue(yaml.contains("additionalProperties: false"), yaml);
|
||||
assertTrue(yaml.contains("owner_org: \"MCP_TOOL\""), yaml);
|
||||
ToolDefinition parsed = new ObjectMapper(new YAMLFactory()).readValue(yaml, ToolDefinition.class);
|
||||
ToolDefinitionValidator.validate(parsed, definition.toString());
|
||||
assertTrue(useCase.contains("name = \"smp_employee_search\""), useCase);
|
||||
assertTrue(useCase.contains("whenToUse = \"사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.\""), useCase);
|
||||
assertTrue(useCase.contains("exampleQueries = {\"직원 조회 정보를 보여줘\""), useCase);
|
||||
assertTrue(useCase.contains("ownerOrg = \"MCP_TOOL\""), useCase);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -178,12 +168,12 @@ class ToolScaffolderTest {
|
||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", List.of("10001"), "", true)),
|
||||
List.of(), "employee", options);
|
||||
|
||||
Path definition = root.resolve("dat-was-v17-options/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||
ToolDefinition parsed = new ObjectMapper(new YAMLFactory()).readValue(Files.readString(definition), ToolDefinition.class);
|
||||
assertEquals("HR_TEAM", parsed.ownerOrg());
|
||||
assertEquals("10001 직원을 찾아줘", parsed.exampleQueries().get(2));
|
||||
assertEquals(2, parsed.tags().size());
|
||||
ToolDefinitionValidator.validate(parsed, definition.toString());
|
||||
Path useCasePath = root.resolve("dat-was-v17-options/src/main/java/io/shinhanlife/dat/mcc/biz/smp/usecase/EmployeeSearchUseCase.java");
|
||||
String useCase = Files.readString(useCasePath);
|
||||
assertTrue(useCase.contains("functionDescription = \"사번으로 직원을 조회한다.\""), useCase);
|
||||
assertTrue(useCase.contains("exampleQueries = {\"사번 10001을 조회해줘\", \"직원 10001 소속을 알려줘\", \"10001 직원을 찾아줘\"}"), useCase);
|
||||
assertTrue(useCase.contains("tags = {\"employee\", \"search\"}"), useCase);
|
||||
assertTrue(useCase.contains("ownerOrg = \"HR_TEAM\""), useCase);
|
||||
}
|
||||
|
||||
@TempDir
|
||||
@@ -201,7 +191,9 @@ class ToolScaffolderTest {
|
||||
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
|
||||
|
||||
assertTrue(useCase.contains("name = \"cmm_claim_search\""));
|
||||
assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")"));
|
||||
assertTrue(useCase.contains("requiresApproval = false"), useCase);
|
||||
assertTrue(useCase.contains("categoryKey = \"cmm\""), useCase);
|
||||
assertTrue(useCase.contains("mappingId = \"CLM0001\""), useCase);
|
||||
assertTrue(response.contains("private String resultCode;"));
|
||||
assertTrue(response.contains("private String resultMessage;"));
|
||||
}
|
||||
@@ -231,9 +223,10 @@ class ToolScaffolderTest {
|
||||
Path schemas = root.resolve("dat-was-sample/src/main/resources/tool-schemas/cmm");
|
||||
assertTrue(Files.exists(schemas.resolve("claim-search-resource-input-schema.json")));
|
||||
assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); String useCase = Files.readString(root.resolve("dat-was-sample/src/main/java/io/shinhanlife/dat/mcc/biz/cmm/usecase/ClaimSearchUseCase.java"));
|
||||
assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\","));
|
||||
assertTrue(useCase.contains("inputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-input-schema.json\""));
|
||||
assertTrue(useCase.contains("outputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-output-schema.json\""));
|
||||
assertTrue(useCase.contains("categoryKey = \"cmm\""), useCase);
|
||||
assertTrue(useCase.contains("mappingId = \"CLM0001\""), useCase);
|
||||
assertTrue(useCase.contains("inputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-input-schema.json\""), useCase);
|
||||
assertTrue(useCase.contains("outputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-output-schema.json\""), useCase);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -272,12 +265,14 @@ class ToolScaffolderTest {
|
||||
Path sourceRoot = root.resolve("dat-was-pay/src/main/java/io/shinhanlife/dat/mcc");
|
||||
String request = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrRequest.java"));
|
||||
String response = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrResponse.java"));
|
||||
String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_I.java"));
|
||||
String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_O.java"));
|
||||
String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_I.java"));
|
||||
String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_O.java"));
|
||||
String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); String useCase = Files.readString(sourceRoot.resolve("biz/pay/usecase/SearchHrUseCase.java"));
|
||||
String implementation = Files.readString(sourceRoot.resolve("biz/pay/usecase/impl/SearchHrUseCaseImpl.java"));
|
||||
assertTrue(mciRequest.contains("package io.shinhanlife.dat.mcc.infra.itrf.mci.dfag.io;"), mciRequest); assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"pay\", mappingId = \"SHEARCH_01\")"));
|
||||
assertTrue(implementation.contains("import io.shinhanlife.dat.mcc.infra.itrf.mci.dfag.io.SHEARCH_01_O;"));
|
||||
assertTrue(mciRequest.contains("package io.shinhanlife.dat.mcc.infra.itrf.mci.dfa.g.io;"), mciRequest);
|
||||
assertTrue(useCase.contains("categoryKey = \"pay\""), useCase);
|
||||
assertTrue(useCase.contains("mappingId = \"SHEARCH_01\""), useCase);
|
||||
assertTrue(implementation.contains("import io.shinhanlife.dat.mcc.infra.itrf.mci.dfa.g.io.SHEARCH_01_O;"));
|
||||
|
||||
assertTrue(request.contains("private String employeeId;"));
|
||||
assertTrue(request.contains("private Integer page;"));
|
||||
@@ -481,7 +476,7 @@ class ToolScaffolderTest {
|
||||
String impl = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
|
||||
assertTrue(useCase.contains("getProfile(CustomerProfileRequest req)"), useCase);
|
||||
assertTrue(useCase.contains("getNotice(CustomerNoticeRequest req)"), useCase);
|
||||
assertTrue(impl.contains("private final CustomerProfileClient customerProfileClient;"), impl);
|
||||
assertTrue(impl.contains("private final MciCstmClient mciCstmClient;"), impl);
|
||||
assertTrue(impl.contains("private final CustomerNoticeClient customerNoticeClient;"), impl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package io.shinhanlife.dat.lib.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.shinhanlife.dat.lib.annotation.McpOutputSchema;
|
||||
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
|
||||
import io.shinhanlife.dat.lib.util.ToolSchemaResolver.ToolSchemaResourceException;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -46,6 +51,29 @@ class ToolSchemaResolverTest {
|
||||
assertTrue(schema.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identifiesMalformedSchemaResourcesAsSchemaResourceFailures() throws Exception {
|
||||
Method method = ResourceSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||
|
||||
ToolSchemaResourceException exception = assertThrows(ToolSchemaResourceException.class, () ->
|
||||
resolver.resolve(method.getAnnotation(McpTool.class),
|
||||
method.getAnnotation(GrowToolHint.class),
|
||||
AutomaticRequest.class));
|
||||
|
||||
assertInstanceOf(IOException.class, exception.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identifiesMissingSchemaResourcesAsSchemaResourceFailures() throws Exception {
|
||||
Method method = MissingResourceSchemaTool.class.getDeclaredMethod("search", AutomaticRequest.class);
|
||||
|
||||
ToolSchemaResourceException exception = assertThrows(ToolSchemaResourceException.class, () ->
|
||||
resolver.resolve(method.getAnnotation(McpTool.class),
|
||||
method.getAnnotation(GrowToolHint.class), AutomaticRequest.class));
|
||||
|
||||
assertTrue(exception.getMessage().contains("missing-tool-schema.json"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> properties(Map<String, Object> schema) {
|
||||
return (Map<String, Object>) schema.get("properties");
|
||||
@@ -69,6 +97,21 @@ class ToolSchemaResolverTest {
|
||||
}
|
||||
}
|
||||
|
||||
static class ResourceSchemaTool {
|
||||
@McpTool(name = "oth_test_resource_schema")
|
||||
@GrowToolHint(
|
||||
inputSchemaResource = "classpath:schemas/malformed-tool-schema.json")
|
||||
void search(AutomaticRequest request) {
|
||||
}
|
||||
}
|
||||
|
||||
static class MissingResourceSchemaTool {
|
||||
@McpTool(name = "oth_test_missing_resource_schema")
|
||||
@GrowToolHint(inputSchemaResource = "classpath:schemas/missing-tool-schema.json")
|
||||
void search(AutomaticRequest request) {
|
||||
}
|
||||
}
|
||||
|
||||
@McpOutputSchema
|
||||
static class SimpleResponse {
|
||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, allowableValues = {"SUCCESS", "FAILURE"})
|
||||
|
||||
@@ -16,21 +16,37 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
||||
class McpRequestHeaderFilterTest {
|
||||
|
||||
@Test
|
||||
void capturesDapmsHeadersOnlyForTheCurrentRequest() throws Exception {
|
||||
void capturesCanonicalToolHeadersOnlyForTheCurrentRequest() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
|
||||
request.addHeader("x-request-id", "request-001");
|
||||
request.addHeader("guid", "guid-001");
|
||||
request.addHeader("X-Guid", "guid-001");
|
||||
request.addHeader("X-Praf-No", "praf-001");
|
||||
request.addHeader("X-Request-Id", "request-001");
|
||||
request.addHeader("X-Request-Time", "2026-08-25T12:34:56+09:00");
|
||||
request.addHeader("X-Vrtl-Praf-No", "virtual-001");
|
||||
request.addHeader("X-App-Code", "app-001");
|
||||
request.addHeader("X-Project-Code", "project-001");
|
||||
request.addHeader("X-User-Ip", "10.0.0.1");
|
||||
request.addHeader("X-Caller-Ip", "10.0.0.2");
|
||||
request.addHeader("X-Caller-Host", "caller.example.test");
|
||||
request.addHeader("X-Channel", "MCP");
|
||||
request.addHeader("X-Agent-Id", "agent-001");
|
||||
request.addHeader("mcp-session-id", "session-001");
|
||||
request.addHeader("employee-no", "ENC(employee)");
|
||||
request.addHeader("virtual-employee-no", "ENC(virtual)");
|
||||
|
||||
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) ->
|
||||
assertThat(asMap(McpRequestHeaderContext.current())).containsExactly(
|
||||
Map.entry("requestId", "request-001"),
|
||||
Map.entry("guid", "guid-001"),
|
||||
Map.entry("mcpSessionId", "session-001"),
|
||||
Map.entry("employeeNo", "ENC(employee)"),
|
||||
Map.entry("virtualEmployeeNo", "ENC(virtual)")));
|
||||
Map.entry("prafNo", "praf-001"),
|
||||
Map.entry("requestId", "request-001"),
|
||||
Map.entry("requestTime", "2026-08-25T12:34:56+09:00"),
|
||||
Map.entry("vrtlPrafNo", "virtual-001"),
|
||||
Map.entry("appCode", "app-001"),
|
||||
Map.entry("projectCode", "project-001"),
|
||||
Map.entry("userIp", "10.0.0.1"),
|
||||
Map.entry("callerIp", "10.0.0.2"),
|
||||
Map.entry("callerHost", "caller.example.test"),
|
||||
Map.entry("channel", "MCP"),
|
||||
Map.entry("agentId", "agent-001"),
|
||||
Map.entry("mcpSessionId", "session-001")));
|
||||
|
||||
assertNull(McpRequestHeaderContext.current());
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestHeader;
|
||||
class BusinessToolControllerHeaderContractTest {
|
||||
|
||||
@Test
|
||||
void receivesTheHeadersForwardedByDapms() {
|
||||
void receivesTheCanonicalToolHeadersAsOptionalValues() {
|
||||
Method method = Arrays.stream(BusinessToolController.class.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals("executeDynamicTool"))
|
||||
.findFirst()
|
||||
@@ -25,6 +25,18 @@ class BusinessToolControllerHeaderContractTest {
|
||||
.toList();
|
||||
|
||||
assertThat(headerNames).containsExactly(
|
||||
"x-request-id", "guid", "mcp-session-id", "employee-no", "virtual-employee-no");
|
||||
"X-Guid",
|
||||
"X-Praf-No",
|
||||
"X-Request-Id",
|
||||
"X-Request-Time",
|
||||
"X-Vrtl-Praf-No",
|
||||
"X-App-Code",
|
||||
"X-Project-Code",
|
||||
"X-User-Ip",
|
||||
"X-Caller-Ip",
|
||||
"X-Caller-Host",
|
||||
"X-Channel",
|
||||
"X-Agent-Id",
|
||||
"mcp-session-id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.shinhanlife.dat.mcc.presentation;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -18,7 +19,21 @@ class ToolTestConsoleResourceTest {
|
||||
assertTrue(html.contains("/tool-manifest"));
|
||||
assertTrue(html.contains("Run saved cases"));
|
||||
assertTrue(html.contains("localStorage"));
|
||||
assertTrue(html.contains("request-id"));
|
||||
assertTrue(html.contains("X-Guid"));
|
||||
assertTrue(html.contains("X-Praf-No"));
|
||||
assertTrue(html.contains("X-Request-Id"));
|
||||
assertTrue(html.contains("X-Request-Time"));
|
||||
assertTrue(html.contains("X-Vrtl-Praf-No"));
|
||||
assertTrue(html.contains("X-App-Code"));
|
||||
assertTrue(html.contains("X-Project-Code"));
|
||||
assertTrue(html.contains("X-User-Ip"));
|
||||
assertTrue(html.contains("X-Caller-Ip"));
|
||||
assertTrue(html.contains("X-Caller-Host"));
|
||||
assertTrue(html.contains("X-Channel"));
|
||||
assertTrue(html.contains("X-Agent-Id"));
|
||||
assertFalse(html.contains("'guid':"));
|
||||
assertFalse(html.contains("'employee-no':"));
|
||||
assertFalse(html.contains("'virtual-employee-no':"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ invalid json
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package io.shinhanlife.dat.mcc.biz.pro.converter;
|
||||
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryRequest;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryResponse;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.io. DATNCSO00001_I;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.io. DATNCSO00001_O;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.ReportingPolicy;
|
||||
|
||||
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
|
||||
public interface PersonalCustomerDetailInquiryConverter {
|
||||
// Field names differ? Add mappings like this before the method.
|
||||
@Mapping(source = "customerId", target = "customerId")
|
||||
DATNCSO00001_I toLegacyRequest(PersonalCustomerDetailInquiryRequest request);
|
||||
|
||||
|
||||
PersonalCustomerDetailInquiryResponse toResponse( DATNCSO00001_O mciRes);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.shinhanlife.dat.mcc.biz.pro.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class PersonalCustomerDetailInquiryRequest {
|
||||
@Schema(description = "조회할 고객의 고유 ID (형식: ^[A-Za-z0-9-]{1,20}$) (예시: 12345, CUST-00123)", example = "12345", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@Pattern(regexp = "^[A-Za-z0-9-]{1,20}$")
|
||||
private String customerId;
|
||||
|
||||
@Schema(description = "고객 이름 (선택적 확인용) (형식: ^[가-힣a-zA-Z\s]{2,20}$) (예시: 김철수, Lee Chul-soo)", example = "김철수")
|
||||
@Pattern(regexp = "^[가-힣a-zA-Z\s]{2,20}$")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "고객 전화번호 (선택적 확인용) (형식: $) (예시: 010-1234-5678, +821012345678)", example = "010-1234-5678")
|
||||
private String phoneNumber;
|
||||
|
||||
@Schema(description = "고객 이메일 주소 (선택적 확인용) (형식: $) (예시: customer@example.com, user@test.co.kr)", example = "customer@example.com")
|
||||
private String email;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.shinhanlife.dat.mcc.biz.pro.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class PersonalCustomerDetailInquiryResponse {
|
||||
private String resultCode;
|
||||
|
||||
private String resultMessage;
|
||||
@Schema(description = "조회된 고객의 고유 ID (예시: 12345, CUST-00123)", example = "12345", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String customerId;
|
||||
|
||||
@Schema(description = "고객의 전체 이름 (예시: 김철수, Lee Chul-soo)", example = "김철수", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "고객의 전화번호 (예시: 010-1234-5678)", example = "010-1234-5678", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String phoneNumber;
|
||||
|
||||
@Schema(description = "고객의 이메일 주소 (예시: customer@example.com)", example = "customer@example.com", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String email;
|
||||
|
||||
@Schema(description = "고객의 주소 정보 (예시: 서울시 강남구 테헤란로 123)", example = "서울시 강남구 테헤란로 123")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "고객의 생년월일 (예시: 1990-01-15)", example = "1990-01-15")
|
||||
private String birthDate;
|
||||
|
||||
@Schema(description = "고객의 멤버십 등급 (예시: GOLD, SILVER, BRONZE)", example = "GOLD")
|
||||
private String membershipLevel;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.shinhanlife.dat.mcc.biz.pro.usecase;
|
||||
|
||||
import org.springaicommunity.mcp.annotation.McpTool;
|
||||
import io.shinhanlife.dat.lib.annotation.GrowToolHint;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryRequest;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryResponse;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.mcc.biz.pro.usecase
|
||||
* @className PersonalCustomerDetailInquiryUseCase
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author jade
|
||||
* @create 2026.08.25
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.08.25 jade 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
public interface PersonalCustomerDetailInquiryUseCase {
|
||||
|
||||
@McpTool(name = "pro_personal_inquiry", title = "개인고객 상세 정보 조회", description = "개인고객의 상세 정보를 조회하는 도구로, 고객 ID 또는 기본 정보를 통해 이름, 연락처, 주소, 생년월일, 멤버십 등급 등 개인 고객 정보를 안전하게 조회할 수 있습니다.")
|
||||
@GrowToolHint(
|
||||
requiresApproval = false,
|
||||
categoryKey = "pro",
|
||||
mappingId = " DATNCSO00001",
|
||||
functionDescription = "고객 ID 또는 기본 정보를 기반으로 개인 고객의 상세 정보를 조회하고 반환하는 핵심 비즈니스 함수입니다.",
|
||||
whenToUse = "개인 고객의 상세 정보가 필요한 경우, 예를 들어 고객 ID, 이름, 전화번호, 이메일 등을 입력하여 고객 정보를 조회하고자 할 때 이 도구를 선택해야 합니다.",
|
||||
whenNotToUse = "개인 고객이 아닌 법인 고객 정보 조회, 익명 또는 비개인 정보 요청, 또는 개인 정보 보호 정책상 정보 조회가 제한된 경우 이 도구를 선택하지 않아야 합니다.",
|
||||
ioLimits = "입력 필드는 고객 ID(필수) 및 선택적 이름, 전화번호, 이메일을 허용하며, 최대 10개의 필드를 포함할 수 있습니다. 출력 필드는 결과 코드, 고객 ID, 이름, 전화번호, 이메일, 주소, 생년월일, 멤버십 등급 등을 포함하며, 최대 10개의 필드를 반환할 수 있습니다.",
|
||||
displayDescription = "개인 고객의 상세 정보를 조회하는 포털용 간략한 설명으로, 이름, 연락처, 주소, 생년월일, 멤버십 등급 등을 확인할 수 있습니다.",
|
||||
exampleQueries = {"고객 ID 12345로 개인 고객 정보를 조회해줘", "김철수 고객의 상세 정보를 알려줘", "010-1234-5678로 등록된 개인 고객 정보를 조회해줘", "lee@example.com 이메일로 가입된 개인 고객 정보를 조회해줘"},
|
||||
destructive = false,
|
||||
idempotent = true,
|
||||
tags = {"고객관리", "조회"},
|
||||
ownerOrg = "MCP_TOOL"
|
||||
)
|
||||
PersonalCustomerDetailInquiryResponse execute(PersonalCustomerDetailInquiryRequest req);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package io.shinhanlife.dat.mcc.biz.pro.usecase.impl;
|
||||
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryRequest;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryResponse;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.usecase.PersonalCustomerDetailInquiryUseCase;
|
||||
import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.converter.PersonalCustomerDetailInquiryConverter;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.io. DATNCSO00001_I;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.io. DATNCSO00001_O;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.MciNcscClient;
|
||||
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.mcc.biz.pro.usecase.impl
|
||||
* @className PersonalCustomerDetailInquiryUseCaseImpl
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author jade
|
||||
* @create 2026.08.25
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.08.25 jade 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PersonalCustomerDetailInquiryUseCaseImpl implements PersonalCustomerDetailInquiryUseCase {
|
||||
|
||||
private final MciNcscClient mci;
|
||||
private final PersonalCustomerDetailInquiryConverter converter;
|
||||
|
||||
@Override
|
||||
public PersonalCustomerDetailInquiryResponse execute(PersonalCustomerDetailInquiryRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신.", "pro_personal_inquiry");
|
||||
try {
|
||||
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||
DATNCSO00001_I mciReq = converter.toLegacyRequest(req);
|
||||
|
||||
Transfer< DATNCSO00001_O> resTransfer = mci.callTo(
|
||||
" DATNCSO00001",
|
||||
null,
|
||||
mciReq,
|
||||
DATNCSO00001_O.class
|
||||
);
|
||||
PersonalCustomerDetailInquiryResponse response = new PersonalCustomerDetailInquiryResponse();
|
||||
if (resTransfer.getBody() != null) {
|
||||
response = converter.toResponse(resTransfer.getBody());
|
||||
}
|
||||
response.setResultCode("SUCCESS");
|
||||
response.setResultMessage(resTransfer.getBody() != null
|
||||
? "MCI call completed."
|
||||
: "MCI call completed without a response body.");
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||
PersonalCustomerDetailInquiryResponse response = new PersonalCustomerDetailInquiryResponse();
|
||||
response.setResultCode("ERROR");
|
||||
response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import io.shinhanlife.dat.lib.integration.mci.component.AxhubMciComponent;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
|
||||
/**
|
||||
* @package io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c
|
||||
* @className MciNcscClient
|
||||
* @description AX HUB 시스템 처리 클래스
|
||||
* @author jade
|
||||
* @create 2026.08.25
|
||||
* <pre>
|
||||
* ---------- 개정이력 ----------
|
||||
* 수정일 수정자 수정내용
|
||||
* ---------- -------- ---------------------------
|
||||
* 2026.08.25 jade 최초생성
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MciNcscClient {
|
||||
private final AxhubMciComponent mci;
|
||||
|
||||
public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> resType) throws Exception {
|
||||
return mci.callTo(interfaceId, dummy, mciReq, resType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.io;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
|
||||
@Data
|
||||
public class DATNCSO00001_I {
|
||||
@Schema(description = "조회할 고객의 고유 ID (형식: ^[A-Za-z0-9-]{1,20}$) (예시: 12345, CUST-00123)", example = "12345", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@Pattern(regexp = "^[A-Za-z0-9-]{1,20}$")
|
||||
private String customerId;
|
||||
|
||||
@Schema(description = "고객 이름 (선택적 확인용) (형식: ^[가-힣a-zA-Z\s]{2,20}$) (예시: 김철수, Lee Chul-soo)", example = "김철수")
|
||||
@Pattern(regexp = "^[가-힣a-zA-Z\s]{2,20}$")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "고객 전화번호 (선택적 확인용) (형식: {4}$) (예시: 010-1234-5678, +821012345678)", example = "010-1234-5678")
|
||||
private String phoneNumber;
|
||||
|
||||
@Schema(description = "고객 이메일 주소 (선택적 확인용) (형식: $) (예시: customer@example.com, user@test.co.kr)", example = "customer@example.com")
|
||||
private String email;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.shinhanlife.dat.mcc.infra.itrf.mci.ncs.c.io;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
@Data
|
||||
public class DATNCSO00001_O {
|
||||
@Schema(description = "조회 결과 상태 코드 (예시: SUCCESS, ERROR_001)", example = "SUCCESS", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String resultCode;
|
||||
|
||||
@Schema(description = "조회된 고객의 고유 ID (예시: 12345, CUST-00123)", example = "12345", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String customerId;
|
||||
|
||||
@Schema(description = "고객의 전체 이름 (예시: 김철수, Lee Chul-soo)", example = "김철수", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String name;
|
||||
|
||||
@Schema(description = "고객의 전화번호 (예시: 010-1234-5678)", example = "010-1234-5678", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String phoneNumber;
|
||||
|
||||
@Schema(description = "고객의 이메일 주소 (예시: customer@example.com)", example = "customer@example.com", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String email;
|
||||
|
||||
@Schema(description = "고객의 주소 정보 (예시: 서울시 강남구 테헤란로 123)", example = "서울시 강남구 테헤란로 123")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "고객의 생년월일 (예시: 1990-01-15)", example = "1990-01-15")
|
||||
private String birthDate;
|
||||
|
||||
@Schema(description = "고객의 멤버십 등급 (예시: GOLD, SILVER, BRONZE)", example = "GOLD")
|
||||
private String membershipLevel;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"resultCode" : "SUCCESS",
|
||||
"customerId" : "12345",
|
||||
"name" : "김철수",
|
||||
"phoneNumber" : "010-1234-5678",
|
||||
"email" : "customer@example.com",
|
||||
"address" : "서울시 강남구 테헤란로 123",
|
||||
"birthDate" : "1990-01-15",
|
||||
"membershipLevel" : "GOLD"
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.shinhanlife.dat.mcc.biz.pro.usecase;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryRequest;
|
||||
import io.shinhanlife.dat.mcc.biz.pro.dto.PersonalCustomerDetailInquiryResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PersonalCustomerDetailInquiryUseCaseTest {
|
||||
|
||||
@Test
|
||||
void createsToolRequestAndResponseDtos() {
|
||||
assertNotNull(new PersonalCustomerDetailInquiryRequest());
|
||||
assertNotNull(new PersonalCustomerDetailInquiryResponse());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import io.shinhanlife.dat.mcc.biz.cmm.dto.ClaimSearchRequest;
|
||||
import io.shinhanlife.dat.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||||
import io.shinhanlife.dat.mcc.biz.cmm.usecase.ClaimSearchUseCase;
|
||||
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -57,7 +58,7 @@ public class ClaimSearchUseCaseImpl implements ClaimSearchUseCase {
|
||||
? "MCI call completed."
|
||||
: "MCI call completed without a response body.");
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
} catch (BizException e) {
|
||||
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||
ClaimSearchResponse response = new ClaimSearchResponse();
|
||||
response.setResultCode("ERROR");
|
||||
|
||||
@@ -24,7 +24,7 @@ import io.shinhanlife.glow.communication.dto.Transfer;
|
||||
public class MciNclaClient {
|
||||
private final AxhubMciComponent mci;
|
||||
|
||||
public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> resType) throws Exception {
|
||||
public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> resType) {
|
||||
return mci.callTo(interfaceId, dummy, mciReq, resType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.shinhanlife.dat.mcc.biz.cmm.usecase.impl;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import io.shinhanlife.dat.mcc.biz.cmm.converter.ClaimSearchConverter;
|
||||
import io.shinhanlife.dat.mcc.biz.cmm.dto.ClaimSearchRequest;
|
||||
import io.shinhanlife.dat.mcc.biz.cmm.dto.ClaimSearchResponse;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncla.MciNclaClient;
|
||||
import io.shinhanlife.dat.mcc.infra.itrf.mci.ncla.io.CLCNNB00001_O;
|
||||
import io.shinhanlife.glow.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ClaimSearchUseCaseImplTest {
|
||||
|
||||
@Test
|
||||
void returnsErrorResponseWhenMciReportsABusinessCommunicationFailure() {
|
||||
MciNclaClient mci = mock(MciNclaClient.class);
|
||||
when(mci.callTo(eq("CLCNNB00001"), any(), any(), eq(CLCNNB00001_O.class)))
|
||||
.thenThrow(bizException());
|
||||
ClaimSearchUseCaseImpl useCase = new ClaimSearchUseCaseImpl(mci, mock(ClaimSearchConverter.class));
|
||||
|
||||
ClaimSearchResponse response = useCase.searchClaim(mock(ClaimSearchRequest.class));
|
||||
|
||||
assertEquals("ERROR", response.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHideUnexpectedRuntimeFailures() {
|
||||
MciNclaClient mci = mock(MciNclaClient.class);
|
||||
when(mci.callTo(eq("CLCNNB00001"), any(), any(), eq(CLCNNB00001_O.class)))
|
||||
.thenThrow(new IllegalStateException("unexpected defect"));
|
||||
ClaimSearchUseCaseImpl useCase = new ClaimSearchUseCaseImpl(mci, mock(ClaimSearchConverter.class));
|
||||
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> useCase.searchClaim(mock(ClaimSearchRequest.class)));
|
||||
}
|
||||
|
||||
private BizException bizException() {
|
||||
return new BizException("CST00477", new String[]{"MCI communication failure"},
|
||||
new RuntimeException("connection closed"));
|
||||
}
|
||||
}
|
||||
73
docs/superpowers/plans/2026-08-25-mcp-header-contract.md
Normal file
73
docs/superpowers/plans/2026-08-25-mcp-header-contract.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# MCP Header Contract Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the legacy Tool Pod request headers with the approved 12-header contract and propagate every value through execution and outbound HTTP calls.
|
||||
|
||||
**Architecture:** `McpRequestHeaders` is the normalized immutable carrier. Both MCP filtering and the legacy REST adapter populate it, `McpToolExecutionService` returns the transport-safe correlation headers, and `AxhubHttpComponent` forwards the complete contract to downstream HTTP services. All headers remain optional.
|
||||
|
||||
**Tech Stack:** Java 21, Spring Web, Jakarta Servlet, JUnit 5, AssertJ, Gradle
|
||||
|
||||
**Spec:** User-approved header list in the 2026-08-25 Codex task.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Canonical headers are exactly `X-Guid`, `X-Praf-No`, `X-Request-Id`, `X-Request-Time`, `X-Vrtl-Praf-No`, `X-App-Code`, `X-Project-Code`, `X-User-Ip`, `X-Caller-Ip`, `X-Caller-Host`, `X-Channel`, and `X-Agent-Id`.
|
||||
- Remove legacy `guid`, `employee-no`, and `virtual-employee-no`; do not retain aliases.
|
||||
- Keep `mcp-session-id` because it is MCP transport metadata rather than a replaced business header.
|
||||
- Do not reject absent headers; capture and propagate values only when present.
|
||||
- Do not commit or push unless the user requests it separately.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Request Capture Contract
|
||||
|
||||
**Files:**
|
||||
- Modify: `dat-was-lib/src/test/java/io/shinhanlife/dat/mcc/mcp/McpRequestHeaderFilterTest.java`
|
||||
- Modify: `dat-was-lib/src/test/java/io/shinhanlife/dat/mcc/presentation/BusinessToolControllerHeaderContractTest.java`
|
||||
- Modify: `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/McpRequestHeaders.java`
|
||||
- Modify: `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/McpRequestHeaderFilter.java`
|
||||
- Modify: `dat-was-lib/src/main/java/io/shinhanlife/dat/mcc/presentation/BusinessToolController.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `McpRequestHeaders` accessors for all 12 canonical values plus `mcpSessionId()`.
|
||||
|
||||
- [ ] Change the filter test fixture to send literal canonical headers and assert every record component.
|
||||
- [ ] Change the controller contract test to assert the exact 12 canonical names plus `mcp-session-id`, all with `required=false`.
|
||||
- [ ] Run the two tests and confirm failure because the record and controller still expose legacy fields.
|
||||
- [ ] Expand the record and populate it from both HTTP entry points.
|
||||
- [ ] Run the two tests and confirm they pass.
|
||||
|
||||
### Task 2: Execution Response and Downstream Propagation
|
||||
|
||||
**Files:**
|
||||
- Modify: `dat-was-lib/src/test/java/io/shinhanlife/dat/lib/mcp/ToolExecutionServiceTest.java`
|
||||
- Modify: `dat-was-lib/src/test/java/io/shinhanlife/dat/lib/integration/http/component/AxhubHttpComponentTest.java`
|
||||
- Modify: `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/mcp/McpToolExecutionService.java`
|
||||
- Modify: `dat-was-lib/src/main/java/io/shinhanlife/dat/lib/integration/http/component/AxhubHttpComponent.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the expanded `McpRequestHeaders` record from Task 1.
|
||||
- Produces: response headers `X-Request-Id`, `X-Guid`, and `mcp-session-id`; downstream calls receive all 12 canonical headers plus the MCP session header.
|
||||
|
||||
- [ ] Update execution and HTTP integration tests with hand-written literal values for every field.
|
||||
- [ ] Run both tests and confirm failure on missing canonical propagation.
|
||||
- [ ] Return canonical correlation headers and forward all present request headers.
|
||||
- [ ] Run both tests and confirm they pass.
|
||||
|
||||
### Task 3: Documentation and Console Contract
|
||||
|
||||
**Files:**
|
||||
- Modify: `README.md`
|
||||
- Modify: `dat-was-lib/src/main/resources/static/tool-test-console.html`
|
||||
- Modify: `dat-was-lib/src/test/java/io/shinhanlife/dat/mcc/presentation/ToolTestConsoleResourceTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the canonical header names from Tasks 1 and 2.
|
||||
- Produces: runnable examples and console calls that no longer send removed headers.
|
||||
|
||||
- [ ] Update the console resource test to require canonical header labels and calls.
|
||||
- [ ] Run the resource test and confirm failure on legacy header text.
|
||||
- [ ] Update README examples/table and console request/response labels.
|
||||
- [ ] Run the resource test and confirm it passes.
|
||||
- [ ] Run `./gradlew.bat :dat-was-lib:test` and confirm the full module suite passes.
|
||||
Reference in New Issue
Block a user