fix: align tool headers
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m51s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 3m51s
This commit is contained in:
@@ -92,9 +92,11 @@ public class AxhubHttpComponent {
|
|||||||
if (inbound == null) {
|
if (inbound == null) {
|
||||||
header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST);
|
header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST);
|
||||||
} else {
|
} else {
|
||||||
putIfPresent(header, "trace-id", inbound.traceId());
|
putIfPresent(header, "x-request-id", inbound.requestId());
|
||||||
putIfPresent(header, "request-id", inbound.requestId());
|
putIfPresent(header, "guid", inbound.guid());
|
||||||
putIfPresent(header, "X-USER-ID", inbound.encryptedEmployeeId());
|
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);
|
header.setReadTimeout(timeout == 0 ? defaultReadTimeout() : timeout);
|
||||||
return header;
|
return header;
|
||||||
|
|||||||
@@ -21,14 +21,15 @@ public class McpRequestHeaderFilter extends OncePerRequestFilter {
|
|||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||||
FilterChain filterChain) throws ServletException, IOException {
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
McpRequestHeaderContext.set(new McpRequestHeaders(
|
McpRequestHeaderContext.set(new McpRequestHeaders(
|
||||||
request.getHeader("X-Request-Id"),
|
request.getHeader("x-request-id"),
|
||||||
request.getHeader("trace-id"),
|
request.getHeader("guid"),
|
||||||
request.getHeader("request-id"),
|
request.getHeader("mcp-session-id"),
|
||||||
request.getHeader("employee-id")));
|
request.getHeader("employee-no"),
|
||||||
|
request.getHeader("virtual-employee-no")));
|
||||||
try {
|
try {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
} finally {
|
} finally {
|
||||||
McpRequestHeaderContext.clear();
|
McpRequestHeaderContext.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package io.shinhanlife.dap.lib.mcp;
|
|||||||
|
|
||||||
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
|
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
|
||||||
public record McpRequestHeaders(
|
public record McpRequestHeaders(
|
||||||
String headerRequestId,
|
|
||||||
String traceId,
|
|
||||||
String requestId,
|
String requestId,
|
||||||
String encryptedEmployeeId) {
|
String guid,
|
||||||
}
|
String mcpSessionId,
|
||||||
|
String employeeNo,
|
||||||
|
String virtualEmployeeNo) {
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,33 +25,34 @@ public class McpToolExecutionService {
|
|||||||
|
|
||||||
public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders,
|
public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders,
|
||||||
Map<String, Object> arguments) {
|
Map<String, Object> arguments) {
|
||||||
String headerRequestId = requestHeaders == null ? null : requestHeaders.headerRequestId();
|
|
||||||
String traceId = requestHeaders == null ? null : requestHeaders.traceId();
|
|
||||||
String requestId = requestHeaders == null ? null : requestHeaders.requestId();
|
String requestId = requestHeaders == null ? null : requestHeaders.requestId();
|
||||||
log.info("[Tool] IN - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName);
|
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);
|
||||||
|
|
||||||
McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName);
|
McpToolMethodRegistry.RegisteredTool resolvedTool = toolMethodRegistry.find(functionName);
|
||||||
if (resolvedTool == null) {
|
if (resolvedTool == null) {
|
||||||
return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, headerRequestId);
|
return error(404, "TOOL_NOT_FOUND", "Tool not found: " + functionName, requestId);
|
||||||
}
|
}
|
||||||
ToolExecutionResult validationFailure = validateInput(resolvedTool, arguments, headerRequestId);
|
ToolExecutionResult validationFailure = validateInput(resolvedTool, arguments, requestId);
|
||||||
if (validationFailure != null) {
|
if (validationFailure != null) {
|
||||||
return validationFailure;
|
return validationFailure;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments));
|
Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments));
|
||||||
ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, headerRequestId);
|
ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, requestId);
|
||||||
if (outputFailure != null) {
|
if (outputFailure != null) {
|
||||||
return outputFailure;
|
return outputFailure;
|
||||||
}
|
}
|
||||||
Map<String, String> headers = new HashMap<>();
|
Map<String, String> headers = new HashMap<>();
|
||||||
if (traceId != null) headers.put("trace-id", traceId);
|
if (requestId != null) headers.put("x-request-id", requestId);
|
||||||
if (requestId != null) headers.put("request-id", requestId);
|
if (guid != null) headers.put("guid", guid);
|
||||||
log.info("[Tool] OUT - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName);
|
if (mcpSessionId != null) headers.put("mcp-session-id", mcpSessionId);
|
||||||
|
log.info("[Tool] OUT - guid: {}, x-request-id: {}, tool: {}", guid, requestId, functionName);
|
||||||
return new ToolExecutionResult(200, methodResult, headers);
|
return new ToolExecutionResult(200, methodResult, headers);
|
||||||
} catch (Exception error) {
|
} catch (Exception error) {
|
||||||
log.error("[Tool] Tool execution failed. tool={}", functionName, error);
|
log.error("[Tool] Tool execution failed. tool={}", functionName, error);
|
||||||
return error(502, "TOOL_ERROR", "Tool execution failed", headerRequestId);
|
return error(502, "TOOL_ERROR", "Tool execution failed", requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,4 +99,4 @@ public class McpToolExecutionService {
|
|||||||
if (requestId != null) body.put("request_id", requestId);
|
if (requestId != null) body.put("request_id", requestId);
|
||||||
return new ToolExecutionResult(statusCode, body, Map.of());
|
return new ToolExecutionResult(statusCode, body, Map.of());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import io.shinhanlife.dap.lib.mcp.security.ApiKeyInterceptor;
|
|||||||
* 수정일 수정자 수정내용
|
* 수정일 수정자 수정내용
|
||||||
* ---------- -------- ---------------------------
|
* ---------- -------- ---------------------------
|
||||||
* 2026.09.01 0986406 최초생성
|
* 2026.09.01 0986406 최초생성
|
||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@@ -33,7 +33,7 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
public void addInterceptors(InterceptorRegistry registry) {
|
public void addInterceptors(InterceptorRegistry registry) {
|
||||||
// 2. 인터셉터 등록 및 검사할 URL 패턴 지정
|
// 2. 인터셉터 등록 및 검사할 URL 패턴 지정
|
||||||
registry.addInterceptor(apiKeyInterceptor)
|
registry.addInterceptor(apiKeyInterceptor)
|
||||||
.addPathPatterns("/rpc/**", "/mcp/api/v1/**") // /rpc/, /mcp/api/v1/ 로 시작하는 모든 API는 API Key 검사 수행!
|
.addPathPatterns("/rpc/**", "/mcp/**")
|
||||||
.excludePathPatterns(
|
.excludePathPatterns(
|
||||||
"/test/**", "/health", "/error", "/mcp/api/v1/admin/**",
|
"/test/**", "/health", "/error", "/mcp/api/v1/admin/**",
|
||||||
"/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", // Swagger UI 경로는 인증 제외
|
"/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", // Swagger UI 경로는 인증 제외
|
||||||
@@ -51,4 +51,4 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
.exposedHeaders("Mcp-Session-Id")
|
.exposedHeaders("Mcp-Session-Id")
|
||||||
.allowCredentials(true);
|
.allowCredentials(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.Map;
|
|||||||
* 수정일 수정자 수정내용
|
* 수정일 수정자 수정내용
|
||||||
* ---------- -------- ---------------------------
|
* ---------- -------- ---------------------------
|
||||||
* 2026.09.01 0986406 최초생성
|
* 2026.09.01 0986406 최초생성
|
||||||
*
|
*
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -39,7 +39,7 @@ public class ApiKeyInterceptor implements HandlerInterceptor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String apiKey = request.getHeader("X-API-KEY");
|
String apiKey = request.getHeader("X-Tool-Server-API-Key");
|
||||||
Map<String, String> validApiKeys = securityProperties.getApiKeys();
|
Map<String, String> validApiKeys = securityProperties.getApiKeys();
|
||||||
|
|
||||||
// 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다.
|
// 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다.
|
||||||
@@ -63,10 +63,10 @@ public class ApiKeyInterceptor implements HandlerInterceptor {
|
|||||||
// 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다.
|
// 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다.
|
||||||
// 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다.
|
// 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다.
|
||||||
MDC.put("tenantId", tenantId);
|
MDC.put("tenantId", tenantId);
|
||||||
|
|
||||||
// 5. 필요시 컨트롤러 로직에서 사용할 수 있도록 Request 속성에도 담아줍니다.
|
// 5. 필요시 컨트롤러 로직에서 사용할 수 있도록 Request 속성에도 담아줍니다.
|
||||||
request.setAttribute("tenantId", tenantId);
|
request.setAttribute("tenantId", tenantId);
|
||||||
|
|
||||||
log.debug(" [보안 통과] API Key 인증 성공 - 접속 테넌트: {}", tenantId);
|
log.debug(" [보안 통과] API Key 인증 성공 - 접속 테넌트: {}", tenantId);
|
||||||
|
|
||||||
return true; // 인증 통과! 컨트롤러로 진행
|
return true; // 인증 통과! 컨트롤러로 진행
|
||||||
@@ -77,4 +77,4 @@ public class ApiKeyInterceptor implements HandlerInterceptor {
|
|||||||
// 6. 메모리 누수를 방지하기 위해 요청 처리가 완전히 끝나면 MDC에서 테넌트 정보를 지워줍니다.
|
// 6. 메모리 누수를 방지하기 위해 요청 처리가 완전히 끝나면 MDC에서 테넌트 정보를 지워줍니다.
|
||||||
MDC.remove("tenantId");
|
MDC.remove("tenantId");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,14 +34,15 @@ public class BusinessToolController {
|
|||||||
@PostMapping("/mcp/{name}")
|
@PostMapping("/mcp/{name}")
|
||||||
public ResponseEntity<?> executeDynamicTool(
|
public ResponseEntity<?> executeDynamicTool(
|
||||||
@PathVariable("name") String functionName,
|
@PathVariable("name") String functionName,
|
||||||
@RequestHeader(value = "X-Request-Id", required = false) String headerRequestId,
|
@RequestHeader(value = "x-request-id", required = false) String requestId,
|
||||||
@RequestHeader(value = "trace-id", required = false) String traceId,
|
@RequestHeader(value = "guid", required = false) String guid,
|
||||||
@RequestHeader(value = "request-id", required = false) String requestId,
|
@RequestHeader(value = "mcp-session-id", required = false) String mcpSessionId,
|
||||||
@RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId,
|
@RequestHeader(value = "employee-no", required = false) String employeeNo,
|
||||||
|
@RequestHeader(value = "virtual-employee-no", required = false) String virtualEmployeeNo,
|
||||||
@RequestBody(required = false) Map<String, Object> arguments) {
|
@RequestBody(required = false) Map<String, Object> arguments) {
|
||||||
ToolExecutionResult result = toolExecutionService.execute(
|
ToolExecutionResult result = toolExecutionService.execute(
|
||||||
functionName,
|
functionName,
|
||||||
new McpRequestHeaders(headerRequestId, traceId, requestId, encryptedEmployeeId),
|
new McpRequestHeaders(requestId, guid, mcpSessionId, employeeNo, virtualEmployeeNo),
|
||||||
arguments);
|
arguments);
|
||||||
ResponseEntity.BodyBuilder response = ResponseEntity.status(result.statusCode());
|
ResponseEntity.BodyBuilder response = ResponseEntity.status(result.statusCode());
|
||||||
result.headers().forEach(response::header);
|
result.headers().forEach(response::header);
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
import io.shinhanlife.dap.lib.config.GlowCommunicationProperties;
|
||||||
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
|
||||||
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
|
||||||
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
|
import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.test.web.client.MockRestServiceServer;
|
import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
@@ -62,6 +67,68 @@ class AxhubHttpComponentTest {
|
|||||||
assertThat(response.status()).isEqualTo("OK");
|
assertThat(response.status()).isEqualTo("OK");
|
||||||
server.verify();
|
server.verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forwardsDapmsHeadersToTheConfiguredHttpService() throws Exception {
|
||||||
|
RestClient.Builder builder = RestClient.builder();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build();
|
||||||
|
AxhubHttpProperties properties = new AxhubHttpProperties();
|
||||||
|
properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition(
|
||||||
|
"status", "https://api.example.test", "/v1", HttpMethod.POST, "application/json", false)));
|
||||||
|
AxhubHttpComponent component = new AxhubHttpComponent(
|
||||||
|
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("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)")));
|
||||||
|
try {
|
||||||
|
assertThat(component.call("status", Map.of(), SampleResponse.class).status()).isEqualTo("OK");
|
||||||
|
server.verify();
|
||||||
|
} finally {
|
||||||
|
clearRequestHeaders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpRequestHeaders headers(Map<String, String> values) {
|
||||||
|
try {
|
||||||
|
Class<?>[] types = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||||
|
.map(component -> component.getType())
|
||||||
|
.toArray(Class<?>[]::new);
|
||||||
|
Object[] arguments = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||||
|
.map(component -> values.get(component.getName()))
|
||||||
|
.toArray();
|
||||||
|
return McpRequestHeaders.class.getDeclaredConstructor(types).newInstance(arguments);
|
||||||
|
} catch (ReflectiveOperationException exception) {
|
||||||
|
throw new AssertionError(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setRequestHeaders(McpRequestHeaders headers) throws Exception {
|
||||||
|
Method method = McpRequestHeaderContext.class.getDeclaredMethod("set", McpRequestHeaders.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(null, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearRequestHeaders() throws Exception {
|
||||||
|
Method method = McpRequestHeaderContext.class.getDeclaredMethod("clear");
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
record SampleResponse(String status) {
|
record SampleResponse(String status) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import io.shinhanlife.dap.lib.config.McpProperties;
|
|||||||
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||||
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Arrays;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springaicommunity.mcp.annotation.McpTool;
|
import org.springaicommunity.mcp.annotation.McpTool;
|
||||||
import org.springaicommunity.mcp.annotation.McpToolParam;
|
import org.springaicommunity.mcp.annotation.McpToolParam;
|
||||||
@@ -27,11 +28,35 @@ class McpToolExecutionServiceTest {
|
|||||||
void convertsArgumentsToDtoAndExecutesTheMatchedTool() {
|
void convertsArgumentsToDtoAndExecutesTheMatchedTool() {
|
||||||
McpToolExecutionService service = serviceWith(new EchoTool());
|
McpToolExecutionService service = serviceWith(new EchoTool());
|
||||||
ToolExecutionResult result = service.execute("sample.cmm.value.echo",
|
ToolExecutionResult result = service.execute("sample.cmm.value.echo",
|
||||||
new McpRequestHeaders("gateway-1", "trace-1", "request-1", "employee-1"), Map.of("value", "hello"));
|
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")),
|
||||||
|
Map.of("value", "hello"));
|
||||||
assertEquals(200, result.statusCode());
|
assertEquals(200, result.statusCode());
|
||||||
assertEquals("hello", ((Map<?, ?>) result.body()).get("value"));
|
assertEquals("hello", ((Map<?, ?>) result.body()).get("value"));
|
||||||
assertEquals("trace-1", result.headers().get("trace-id"));
|
assertEquals("request-1", result.headers().get("x-request-id"));
|
||||||
assertEquals("request-1", result.headers().get("request-id"));
|
assertEquals("guid-1", result.headers().get("guid"));
|
||||||
|
assertEquals("session-1", result.headers().get("mcp-session-id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpRequestHeaders headers(Map<String, String> values) {
|
||||||
|
try {
|
||||||
|
Class<?>[] types = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||||
|
.map(component -> component.getType())
|
||||||
|
.toArray(Class<?>[]::new);
|
||||||
|
Object[] arguments = Arrays.stream(McpRequestHeaders.class.getRecordComponents())
|
||||||
|
.map(component -> values.get(component.getName()))
|
||||||
|
.toArray();
|
||||||
|
return McpRequestHeaders.class.getDeclaredConstructor(types).newInstance(arguments);
|
||||||
|
} catch (ReflectiveOperationException exception) {
|
||||||
|
throw new AssertionError(exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private McpToolExecutionService serviceWith(Object toolBean) {
|
private McpToolExecutionService serviceWith(Object toolBean) {
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ class ToolScaffolderTest {
|
|||||||
new ToolScaffolder.ToolMethodDefinition(
|
new ToolScaffolder.ToolMethodDefinition(
|
||||||
"CustomerGuidance", "searchGuidance", "CTMNILO00007", "Customer guidance", "Search guidance", "cmm", "MCI",
|
"CustomerGuidance", "searchGuidance", "CTMNILO00007", "Customer guidance", "Search guidance", "cmm", "MCI",
|
||||||
false, "NILD", null,
|
false, "NILD", null,
|
||||||
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)),
|
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", List.of("C001"), "", true)),
|
||||||
List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", "OPEN", false)), null),
|
List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", List.of("OPEN"), "", false)), null),
|
||||||
new ToolScaffolder.ToolMethodDefinition(
|
new ToolScaffolder.ToolMethodDefinition(
|
||||||
"CustomerContract", "searchContract", "CTMCNT00001", "Customer contract", "Search contract", "cmm", "MCI",
|
"CustomerContract", "searchContract", "CTMCNT00001", "Customer contract", "Search contract", "cmm", "MCI",
|
||||||
false, "CNTD", null,
|
false, "CNTD", null,
|
||||||
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)),
|
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", List.of("C001"), "", true)),
|
||||||
List.of(new ToolScaffolder.FieldDefinition("contractStatus", "String", "Contract status", "ACTIVE", false)), null)));
|
List.of(new ToolScaffolder.FieldDefinition("contractStatus", "String", "Contract status", List.of("ACTIVE"), "", false)), null)));
|
||||||
|
|
||||||
Path sourceRoot = root.resolve("dap-was-customer/src/main/java/io/shinhanlife/dap/mcc");
|
Path sourceRoot = root.resolve("dap-was-customer/src/main/java/io/shinhanlife/dap/mcc");
|
||||||
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
|
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
|
||||||
@@ -59,8 +59,8 @@ class ToolScaffolderTest {
|
|||||||
new ToolScaffolder.ToolMethodDefinition(
|
new ToolScaffolder.ToolMethodDefinition(
|
||||||
"EmployeeSearch", "searchEmployee", null, "Employee search", "Search employee", "smp", "HTTP",
|
"EmployeeSearch", "searchEmployee", null, "Employee search", "Search employee", "smp", "HTTP",
|
||||||
false, null, "employee-search",
|
false, null, "employee-search",
|
||||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "Employee number", "10001", true)),
|
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "Employee number", List.of("10001"), "", true)),
|
||||||
List.of(new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong", false)), null)));
|
List.of(new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong"), "", false)), null)));
|
||||||
|
|
||||||
Path glowConfig = root.resolve("dap-was-http/src/main/resources/glow/application-glow-local.yml");
|
Path glowConfig = root.resolve("dap-was-http/src/main/resources/glow/application-glow-local.yml");
|
||||||
assertTrue(Files.exists(glowConfig));
|
assertTrue(Files.exists(glowConfig));
|
||||||
@@ -71,13 +71,13 @@ class ToolScaffolderTest {
|
|||||||
void generatesEnumAndListFieldsInDtoSchemaAndMockResponse() throws Exception {
|
void generatesEnumAndListFieldsInDtoSchemaAndMockResponse() throws Exception {
|
||||||
String moduleName = root.resolve("dap-was-claim").toString();
|
String moduleName = root.resolve("dap-was-claim").toString();
|
||||||
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("claimStatus", "Enum", "Claim status", "OPEN", true,
|
new ToolScaffolder.FieldDefinition("claimStatus", "Enum", "Claim status", List.of("OPEN"), "", true,
|
||||||
List.of("OPEN", "CLOSED"), null, List.of()),
|
List.of("OPEN", "CLOSED"), null, List.of()),
|
||||||
new ToolScaffolder.FieldDefinition("customerIds", "List", "Customer IDs", "C001", false,
|
new ToolScaffolder.FieldDefinition("customerIds", "List", "Customer IDs", List.of("C001"), "", false,
|
||||||
List.of(), "String", List.of()));
|
List.of(), "String", List.of()));
|
||||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", "", false,
|
new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", List.of(), "", false,
|
||||||
List.of(), "Object", List.of(new ToolScaffolder.FieldDefinition("status", "String", "Status", "OPEN", true))));
|
List.of(), "Object", List.of(new ToolScaffolder.FieldDefinition("status", "String", "Status", List.of("OPEN"), "", true))));
|
||||||
|
|
||||||
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
|
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
|
||||||
"tester", "2026.08.12", false, "CLM1", null, null, inputFields, outputFields);
|
"tester", "2026.08.12", false, "CLM1", null, null, inputFields, outputFields);
|
||||||
@@ -107,8 +107,8 @@ class ToolScaffolderTest {
|
|||||||
"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.",
|
"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.",
|
||||||
"cmm", "MCI", moduleName, "테스터", "2026.08.12",
|
"cmm", "MCI", moduleName, "테스터", "2026.08.12",
|
||||||
false, null, null, null,
|
false, null, null, null,
|
||||||
List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", "계약 분석", true)),
|
List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", List.of("계약 분석"), "", true)),
|
||||||
List.of(new ToolScaffolder.FieldDefinition("analysisResult", "String", "분석 결과", "정상", false)));
|
List.of(new ToolScaffolder.FieldDefinition("analysisResult", "String", "분석 결과", List.of("정상"), "", false)));
|
||||||
|
|
||||||
Path sourceRoot = root.resolve("dap-was-korean/src/main/java/io/shinhanlife/dap/mcc");
|
Path sourceRoot = root.resolve("dap-was-korean/src/main/java/io/shinhanlife/dap/mcc");
|
||||||
Path useCase = sourceRoot.resolve("biz/cmm/usecase/AnalysisDataQueryUseCase.java");
|
Path useCase = sourceRoot.resolve("biz/cmm/usecase/AnalysisDataQueryUseCase.java");
|
||||||
@@ -146,7 +146,7 @@ class ToolScaffolderTest {
|
|||||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회",
|
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회",
|
||||||
"사번으로 재직 중인 직원을 조회한다.", "smp", "HTTP", moduleName,
|
"사번으로 재직 중인 직원을 조회한다.", "smp", "HTTP", moduleName,
|
||||||
"tester", "2026.08.12", false, null, null, null,
|
"tester", "2026.08.12", false, null, null, null,
|
||||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", "09860000", true)),
|
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", List.of("09860000"), "", true)),
|
||||||
List.of(), "employee");
|
List.of(), "employee");
|
||||||
|
|
||||||
Path definition = root.resolve("dap-was-v17-definition/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
Path definition = root.resolve("dap-was-v17-definition/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||||
@@ -175,7 +175,7 @@ class ToolScaffolderTest {
|
|||||||
|
|
||||||
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회", "직원을 조회한다.",
|
ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회", "직원을 조회한다.",
|
||||||
"smp", "HTTP", moduleName, "tester", "2026.08.12", false, null, null, null,
|
"smp", "HTTP", moduleName, "tester", "2026.08.12", false, null, null, null,
|
||||||
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", "10001", true)),
|
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "조회할 사번", List.of("10001"), "", true)),
|
||||||
List.of(), "employee", options);
|
List.of(), "employee", options);
|
||||||
|
|
||||||
Path definition = root.resolve("dap-was-v17-options/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
Path definition = root.resolve("dap-was-v17-options/src/main/resources/tool-definitions/smp/smp_employee_search.yml");
|
||||||
@@ -261,10 +261,10 @@ class ToolScaffolderTest {
|
|||||||
void generatesMciToolFromDeclaredInputAndOutputFields() throws Exception {
|
void generatesMciToolFromDeclaredInputAndOutputFields() throws Exception {
|
||||||
String moduleName = root.resolve("dap-was-pay").toString();
|
String moduleName = root.resolve("dap-was-pay").toString();
|
||||||
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true),
|
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", List.of("EMP10001"), "", true),
|
||||||
new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", "1", false));
|
new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", List.of("1"), "", false));
|
||||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong Gildong", true));
|
new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong Gildong"), "", true));
|
||||||
|
|
||||||
ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR search", "pay", "MCI", moduleName,
|
ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR search", "pay", "MCI", moduleName,
|
||||||
"tester", "2026.08.09", true, "DFAG", null, null, inputFields, outputFields);
|
"tester", "2026.08.09", true, "DFAG", null, null, inputFields, outputFields);
|
||||||
@@ -291,7 +291,7 @@ class ToolScaffolderTest {
|
|||||||
void generatesMockResponseAndUnitTestSkeletonFromOutputFields() throws Exception {
|
void generatesMockResponseAndUnitTestSkeletonFromOutputFields() throws Exception {
|
||||||
String moduleName = root.resolve("dap-was-cus").toString();
|
String moduleName = root.resolve("dap-was-cus").toString();
|
||||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("status", "String", "Claim status", "RECEIVED", true));
|
new ToolScaffolder.FieldDefinition("status", "String", "Claim status", List.of("RECEIVED"), "", true));
|
||||||
|
|
||||||
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
|
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
|
||||||
"tester", "2026.08.10", true, null, null, null, List.of(), outputFields);
|
"tester", "2026.08.10", true, null, null, null, List.of(), outputFields);
|
||||||
@@ -318,12 +318,12 @@ class ToolScaffolderTest {
|
|||||||
void generatesDtoPackageAndRemovesDuplicateResponseFields() throws Exception {
|
void generatesDtoPackageAndRemovesDuplicateResponseFields() throws Exception {
|
||||||
String moduleName = root.resolve("dap-was-http").toString();
|
String moduleName = root.resolve("dap-was-http").toString();
|
||||||
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("resultCode", "String", "API result", "SUCCESS", true),
|
new ToolScaffolder.FieldDefinition("resultCode", "String", "API result", List.of("SUCCESS"), "", true),
|
||||||
new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong Gildong", false),
|
new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong Gildong"), "", false),
|
||||||
new ToolScaffolder.FieldDefinition("employeeName", "String", "Duplicate name", "Duplicate", false));
|
new ToolScaffolder.FieldDefinition("employeeName", "String", "Duplicate name", List.of("Duplicate"), "", false));
|
||||||
|
|
||||||
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true));
|
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", List.of("EMP10001"), "", true));
|
||||||
String resultLog = ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "Employee search", "smp", "HTTP", moduleName,
|
String resultLog = ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "Employee search", "smp", "HTTP", moduleName,
|
||||||
"tester", "2026.08.11", false, null, null, null, inputFields, outputFields);
|
"tester", "2026.08.11", false, null, null, null, inputFields, outputFields);
|
||||||
|
|
||||||
@@ -434,10 +434,10 @@ class ToolScaffolderTest {
|
|||||||
void generatesObjectListAsNestedInnerClassWithoutSeparateItemSource() throws Exception {
|
void generatesObjectListAsNestedInnerClassWithoutSeparateItemSource() throws Exception {
|
||||||
String moduleName = root.resolve("dap-was-inner-list").toString();
|
String moduleName = root.resolve("dap-was-inner-list").toString();
|
||||||
List<ToolScaffolder.FieldDefinition> fields = List.of(
|
List<ToolScaffolder.FieldDefinition> fields = List.of(
|
||||||
new ToolScaffolder.FieldDefinition("data", "List", "activity data", "", false,
|
new ToolScaffolder.FieldDefinition("data", "List", "activity data", List.of(), "", false,
|
||||||
List.of(), "Object", List.of(
|
List.of(), "Object", List.of(
|
||||||
new ToolScaffolder.FieldDefinition("date", "String", "date", "2026-08-13", true),
|
new ToolScaffolder.FieldDefinition("date", "String", "date", List.of("2026-08-13"), "", true),
|
||||||
new ToolScaffolder.FieldDefinition("users", "Integer", "users", "10", false))));
|
new ToolScaffolder.FieldDefinition("users", "Integer", "users", List.of("10"), "", false))));
|
||||||
|
|
||||||
ToolScaffolder.scaffold("ga activity status", "GA001", "GA status", "GA status", "ana", "HTTP",
|
ToolScaffolder.scaffold("ga activity status", "GA001", "GA status", "GA status", "ana", "HTTP",
|
||||||
moduleName, "tester", "2026.08.13", false, null, null, null, List.of(), fields);
|
moduleName, "tester", "2026.08.13", false, null, null, null, List.of(), fields);
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package io.shinhanlife.dap.lib.manifest;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
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.dap.lib.config.McpProperties;
|
|
||||||
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
class ToolManifestServiceTest {
|
|
||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void buildsStandardManifestAndDerivesRevisionFromToolDefinition() throws Exception {
|
|
||||||
McpProperties properties = manifestProperties("insurance-processing", "processing.");
|
|
||||||
ToolManifestService service = new ToolManifestService(
|
|
||||||
() -> List.of(tool("processing.contract.inquiry", "1.2.0", 3000)), objectMapper, properties);
|
|
||||||
|
|
||||||
ToolManifestResponse manifest = service.currentManifest();
|
|
||||||
|
|
||||||
assertEquals("insurance-processing", manifest.bundleId());
|
|
||||||
assertTrue(manifest.revision().matches("\\d+"));
|
|
||||||
assertEquals(1, manifest.tools().size());
|
|
||||||
ToolManifestItem item = manifest.tools().getFirst();
|
|
||||||
assertEquals("processing.contract.inquiry", item.name());
|
|
||||||
assertEquals("http://tool-processing.ax-hub.svc.cluster.local:8080/mcp/processing.contract.inquiry",
|
|
||||||
item.endpoint());
|
|
||||||
assertEquals("계약 조회", item.title());
|
|
||||||
assertEquals("object", item.inputSchema().get("type"));
|
|
||||||
assertTrue(item.annotations().readOnlyHint());
|
|
||||||
assertEquals("1.2.0", item.meta().version());
|
|
||||||
assertEquals(3000, item.meta().timeoutMillis());
|
|
||||||
assertEquals("when to use", item.meta().whenToUse());
|
|
||||||
assertEquals("when not to use", item.meta().whenNotToUse());
|
|
||||||
assertEquals("io limits", item.meta().ioLimits());
|
|
||||||
String payload = objectMapper.writeValueAsString(manifest);
|
|
||||||
assertTrue(payload.contains("\"when_to_use\":\"when to use\""));
|
|
||||||
assertTrue(payload.contains("\"when_not_to_use\":\"when not to use\""));
|
|
||||||
assertTrue(payload.contains("\"io_limits\":\"io limits\""));
|
|
||||||
assertEquals(List.of("계약 상태를 알려줘", "내 계약을 조회해줘", "계약번호로 찾아줘"),
|
|
||||||
item.meta().exampleQueries());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void changesRevisionWhenToolDefinitionChanges() {
|
|
||||||
McpProperties properties = manifestProperties("insurance-processing", "processing.");
|
|
||||||
ToolManifestService before = new ToolManifestService(
|
|
||||||
() -> List.of(tool("processing.contract.inquiry", "1.2.0", 3000)), objectMapper, properties);
|
|
||||||
ToolManifestService after = new ToolManifestService(
|
|
||||||
() -> List.of(tool("processing.contract.inquiry", "1.2.0", 5000)), objectMapper, properties);
|
|
||||||
|
|
||||||
assertTrue(!before.currentManifest().revision().equals(after.currentManifest().revision()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rejectsEntireManifestWhenToolNameDoesNotMatchConfiguredPrefix() {
|
|
||||||
McpProperties properties = manifestProperties("insurance-processing", "processing.");
|
|
||||||
ToolManifestService service = new ToolManifestService(
|
|
||||||
() -> List.of(tool("notification_sms_send", "1.0.0", 3000)), objectMapper, properties);
|
|
||||||
|
|
||||||
IllegalStateException error = assertThrows(IllegalStateException.class, service::currentManifest);
|
|
||||||
|
|
||||||
assertTrue(error.getMessage().contains("name-prefix"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void rejectsEntireManifestWhenToolNamesAreDuplicated() {
|
|
||||||
McpProperties properties = manifestProperties("insurance-processing", "processing.");
|
|
||||||
ToolManifestService service = new ToolManifestService(
|
|
||||||
() -> List.of(tool("processing.contract.inquiry", "1.0.0", 3000),
|
|
||||||
tool("processing.contract.inquiry", "1.0.1", 3000)), objectMapper, properties);
|
|
||||||
|
|
||||||
IllegalStateException error = assertThrows(IllegalStateException.class, service::currentManifest);
|
|
||||||
|
|
||||||
assertTrue(error.getMessage().contains("duplicate"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private McpProperties manifestProperties(String bundleId, String namePrefix) {
|
|
||||||
McpProperties properties = new McpProperties();
|
|
||||||
McpProperties.Manifest manifest = new McpProperties.Manifest();
|
|
||||||
manifest.setBundleId(bundleId);
|
|
||||||
manifest.setNamePrefix(namePrefix);
|
|
||||||
properties.setManifest(manifest);
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ToolMetadata tool(String name, String version, long timeoutMillis) {
|
|
||||||
return ToolMetadata.builder()
|
|
||||||
.name(name)
|
|
||||||
.podUrl("http://tool-processing.ax-hub.svc.cluster.local:8080")
|
|
||||||
.displayName("계약 조회")
|
|
||||||
.description("계약번호로 계약 정보를 조회합니다.")
|
|
||||||
.exampleQueries(List.of("계약 상태를 알려줘", "내 계약을 조회해줘", "계약번호로 찾아줘"))
|
|
||||||
.tags(List.of("계약"))
|
|
||||||
.ownerOrg("MCP_TOOL")
|
|
||||||
.whenToUse("when to use")
|
|
||||||
.whenNotToUse("when not to use")
|
|
||||||
.ioLimits("io limits")
|
|
||||||
.parametersSchema(Map.of("type", "object", "properties", Map.of("contractNo", Map.of("type", "string")),
|
|
||||||
"required", List.of("contractNo"), "additionalProperties", false))
|
|
||||||
.semver(version)
|
|
||||||
.timeoutMillis(timeoutMillis)
|
|
||||||
.enabled(true)
|
|
||||||
.readOnlyHint(true)
|
|
||||||
.destructiveHint(false)
|
|
||||||
.idempotentHint(true)
|
|
||||||
.openWorldHint(false)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
package io.shinhanlife.dap.mcc.mcp;
|
package io.shinhanlife.dap.mcc.mcp;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.doReturn;
|
|
||||||
import static org.mockito.Mockito.mock;
|
|
||||||
import static org.mockito.Mockito.verify;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
|
||||||
import io.modelcontextprotocol.server.McpSyncServer;
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderFilter;
|
||||||
import io.shinhanlife.dap.lib.mcp.*;
|
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.RecordComponent;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
@@ -20,41 +16,34 @@ import org.springframework.mock.web.MockHttpServletResponse;
|
|||||||
class McpRequestHeaderFilterTest {
|
class McpRequestHeaderFilterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void capturesOptionalMcpHeadersOnlyForTheCurrentRequest() throws Exception {
|
void capturesDapmsHeadersOnlyForTheCurrentRequest() throws Exception {
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
|
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
|
||||||
request.addHeader("X-Request-Id", "gateway-request-id");
|
request.addHeader("x-request-id", "request-001");
|
||||||
request.addHeader("trace-id", "trace-001");
|
request.addHeader("guid", "guid-001");
|
||||||
request.addHeader("request-id", "tool-request-001");
|
request.addHeader("mcp-session-id", "session-001");
|
||||||
request.addHeader("employee-id", "encrypted-employee-id");
|
request.addHeader("employee-no", "ENC(employee)");
|
||||||
|
request.addHeader("virtual-employee-no", "ENC(virtual)");
|
||||||
|
|
||||||
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> {
|
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) ->
|
||||||
McpRequestHeaders headers = McpRequestHeaderContext.current();
|
assertThat(asMap(McpRequestHeaderContext.current())).containsExactly(
|
||||||
assertEquals("gateway-request-id", headers.headerRequestId());
|
Map.entry("requestId", "request-001"),
|
||||||
assertEquals("trace-001", headers.traceId());
|
Map.entry("guid", "guid-001"),
|
||||||
assertEquals("tool-request-001", headers.requestId());
|
Map.entry("mcpSessionId", "session-001"),
|
||||||
assertEquals("encrypted-employee-id", headers.encryptedEmployeeId());
|
Map.entry("employeeNo", "ENC(employee)"),
|
||||||
});
|
Map.entry("virtualEmployeeNo", "ENC(virtual)")));
|
||||||
|
|
||||||
assertNull(McpRequestHeaderContext.current());
|
assertNull(McpRequestHeaderContext.current());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
private Map<String, Object> asMap(McpRequestHeaders headers) {
|
||||||
void forwardsCapturedHeadersToSharedToolExecutionService() throws Exception {
|
try {
|
||||||
McpToolExecutionService service = mock(McpToolExecutionService.class);
|
Map<String, Object> values = new LinkedHashMap<>();
|
||||||
McpRequestHeaders headers = new McpRequestHeaders(
|
for (RecordComponent component : McpRequestHeaders.class.getRecordComponents()) {
|
||||||
"gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id");
|
values.put(component.getName(), component.getAccessor().invoke(headers));
|
||||||
doReturn(new ToolExecutionResult(200, Map.of("result", "ok"), Map.of()))
|
}
|
||||||
.when(service).execute(eq("sampleTool"), eq(headers), eq(Map.of("key", "value")));
|
return values;
|
||||||
ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer(
|
} catch (ReflectiveOperationException exception) {
|
||||||
mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), service, new ObjectMapper());
|
throw new AssertionError(exception);
|
||||||
|
}
|
||||||
Method invoke = ToolPodMcpToolSynchronizer.class.getDeclaredMethod(
|
|
||||||
"invoke", String.class, McpRequestHeaders.class, Map.class);
|
|
||||||
invoke.setAccessible(true);
|
|
||||||
invoke.invoke(synchronizer, "sampleTool",
|
|
||||||
headers,
|
|
||||||
Map.of("key", "value"));
|
|
||||||
|
|
||||||
verify(service).execute("sampleTool", headers, Map.of("key", "value"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
package io.shinhanlife.dap.mcc.presentation;
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Parameter;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
|
||||||
class BusinessToolControllerHeaderContractTest {
|
class BusinessToolControllerHeaderContractTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void encryptedEmployeeIdHeaderIsOptional() throws Exception {
|
void receivesTheHeadersForwardedByDapms() {
|
||||||
Method method = BusinessToolController.class.getDeclaredMethod(
|
Method method = Arrays.stream(BusinessToolController.class.getDeclaredMethods())
|
||||||
"executeDynamicTool",
|
.filter(candidate -> candidate.getName().equals("executeDynamicTool"))
|
||||||
String.class,
|
.findFirst()
|
||||||
String.class,
|
.orElseThrow();
|
||||||
String.class,
|
|
||||||
String.class,
|
|
||||||
String.class,
|
|
||||||
Map.class);
|
|
||||||
|
|
||||||
Parameter employeeIdParameter = method.getParameters()[4];
|
List<String> headerNames = Arrays.stream(method.getParameters())
|
||||||
RequestHeader requestHeader = employeeIdParameter.getAnnotation(RequestHeader.class);
|
.map(parameter -> parameter.getAnnotation(RequestHeader.class))
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.peek(header -> assertThat(header.required()).isFalse())
|
||||||
|
.map(RequestHeader::value)
|
||||||
|
.toList();
|
||||||
|
|
||||||
assertEquals("employee-id", requestHeader.value());
|
assertThat(headerNames).containsExactly(
|
||||||
assertFalse(requestHeader.required());
|
"x-request-id", "guid", "mcp-session-id", "employee-no", "virtual-employee-no");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: dap-was-pro/Dockerfile
|
dockerfile: dap-was-pro/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8085:8085"
|
- "8285:8085"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Seoul
|
- TZ=Asia/Seoul
|
||||||
- AXHUB_GATEWAY_URL=http://gateway:8081
|
- AXHUB_GATEWAY_URL=http://gateway:8081
|
||||||
@@ -132,7 +132,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: dap-was-sys/Dockerfile
|
dockerfile: dap-was-sys/Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8086:8086"
|
- "8286:8086"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Seoul
|
- TZ=Asia/Seoul
|
||||||
- AXHUB_GATEWAY_URL=http://gateway:8081
|
- AXHUB_GATEWAY_URL=http://gateway:8081
|
||||||
|
|||||||
Reference in New Issue
Block a user