From 406e3c79b7cbc3eb3676fe1b47778b28dbd572e4 Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 18 Aug 2026 09:52:54 +0900 Subject: [PATCH] fix: align tool headers --- .../http/component/AxhubHttpComponent.java | 8 +- .../dap/lib/mcp/McpRequestHeaderFilter.java | 11 +- .../dap/lib/mcp/McpRequestHeaders.java | 9 +- .../dap/lib/mcp/McpToolExecutionService.java | 23 ++-- .../dap/lib/mcp/config/WebConfig.java | 6 +- .../lib/mcp/security/ApiKeyInterceptor.java | 10 +- .../presentation/BusinessToolController.java | 11 +- .../component/AxhubHttpComponentTest.java | 67 ++++++++++ .../dap/lib/mcp/ToolExecutionServiceTest.java | 31 ++++- .../dap/lib/util/ToolScaffolderTest.java | 50 ++++---- .../mcc/manifest/ToolManifestServiceTest.java | 115 ------------------ .../mcc/mcp/McpRequestHeaderFilterTest.java | 69 +++++------ ...inessToolControllerHeaderContractTest.java | 36 +++--- docker-compose.yml | 4 +- 14 files changed, 210 insertions(+), 240 deletions(-) delete mode 100644 dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java index dccf1fe5..d1b9e330 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java @@ -92,9 +92,11 @@ public class AxhubHttpComponent { if (inbound == null) { header.set("X-ANONYMOUS-REQ", ANONYMOUS_REQUEST); } else { - putIfPresent(header, "trace-id", inbound.traceId()); - putIfPresent(header, "request-id", inbound.requestId()); - putIfPresent(header, "X-USER-ID", inbound.encryptedEmployeeId()); + putIfPresent(header, "x-request-id", inbound.requestId()); + putIfPresent(header, "guid", inbound.guid()); + 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; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java index aae2654f..b7c8d896 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaderFilter.java @@ -21,14 +21,15 @@ public class McpRequestHeaderFilter extends OncePerRequestFilter { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { McpRequestHeaderContext.set(new McpRequestHeaders( - request.getHeader("X-Request-Id"), - request.getHeader("trace-id"), - request.getHeader("request-id"), - request.getHeader("employee-id"))); + request.getHeader("x-request-id"), + request.getHeader("guid"), + request.getHeader("mcp-session-id"), + request.getHeader("employee-no"), + request.getHeader("virtual-employee-no"))); try { filterChain.doFilter(request, response); } finally { McpRequestHeaderContext.clear(); } } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java index 3eb87c76..a4a0a3e2 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpRequestHeaders.java @@ -2,8 +2,9 @@ package io.shinhanlife.dap.lib.mcp; /** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */ public record McpRequestHeaders( - String headerRequestId, - String traceId, String requestId, - String encryptedEmployeeId) { -} \ No newline at end of file + String guid, + String mcpSessionId, + String employeeNo, + String virtualEmployeeNo) { +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java index 6b0c6cfb..2a59d397 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/McpToolExecutionService.java @@ -25,33 +25,34 @@ public class McpToolExecutionService { public ToolExecutionResult execute(String functionName, McpRequestHeaders requestHeaders, Map arguments) { - String headerRequestId = requestHeaders == null ? null : requestHeaders.headerRequestId(); - String traceId = requestHeaders == null ? null : requestHeaders.traceId(); 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); 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) { return validationFailure; } try { Object methodResult = invoke(resolvedTool, convertArgument(resolvedTool.method(), arguments)); - ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, headerRequestId); + ToolExecutionResult outputFailure = validateOutput(resolvedTool, methodResult, requestId); if (outputFailure != null) { return outputFailure; } Map headers = new HashMap<>(); - if (traceId != null) headers.put("trace-id", traceId); - if (requestId != null) headers.put("request-id", requestId); - log.info("[Tool] OUT - trace-id: {}, request-id: {}, tool: {}", traceId, requestId, functionName); + if (requestId != null) headers.put("x-request-id", requestId); + if (guid != null) headers.put("guid", guid); + 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); } catch (Exception 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); return new ToolExecutionResult(statusCode, body, Map.of()); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java index 9e3cfc8b..437a056f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/config/WebConfig.java @@ -19,7 +19,7 @@ import io.shinhanlife.dap.lib.mcp.security.ApiKeyInterceptor; * 수정일 수정자 수정내용 * ---------- -------- --------------------------- * 2026.09.01 0986406 최초생성 - * + * * */ @Configuration @@ -33,7 +33,7 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { // 2. 인터셉터 등록 및 검사할 URL 패턴 지정 registry.addInterceptor(apiKeyInterceptor) - .addPathPatterns("/rpc/**", "/mcp/api/v1/**") // /rpc/, /mcp/api/v1/ 로 시작하는 모든 API는 API Key 검사 수행! + .addPathPatterns("/rpc/**", "/mcp/**") .excludePathPatterns( "/test/**", "/health", "/error", "/mcp/api/v1/admin/**", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", // Swagger UI 경로는 인증 제외 @@ -51,4 +51,4 @@ public class WebConfig implements WebMvcConfigurer { .exposedHeaders("Mcp-Session-Id") .allowCredentials(true); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java index 89fb3db5..a4c0295b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/mcp/security/ApiKeyInterceptor.java @@ -21,7 +21,7 @@ import java.util.Map; * 수정일 수정자 수정내용 * ---------- -------- --------------------------- * 2026.09.01 0986406 최초생성 - * + * * */ @Slf4j @@ -39,7 +39,7 @@ public class ApiKeyInterceptor implements HandlerInterceptor { return true; } - String apiKey = request.getHeader("X-API-KEY"); + String apiKey = request.getHeader("X-Tool-Server-API-Key"); Map validApiKeys = securityProperties.getApiKeys(); // 2. 만약 프로퍼티에 API Key가 하나도 설정되어 있지 않다면 (개발/로컬 환경 등) 인증 없이 통과시킵니다. @@ -63,10 +63,10 @@ public class ApiKeyInterceptor implements HandlerInterceptor { // 4. 추출한 Tenant ID를 현재 스레드의 로깅 컨텍스트(MDC)에 저장합니다. // 이렇게 하면 이 요청이 끝날 때까지 찍히는 모든 로그에 어떤 테넌트가 호출했는지 자동으로 기록됩니다. MDC.put("tenantId", tenantId); - + // 5. 필요시 컨트롤러 로직에서 사용할 수 있도록 Request 속성에도 담아줍니다. request.setAttribute("tenantId", tenantId); - + log.debug(" [보안 통과] API Key 인증 성공 - 접속 테넌트: {}", tenantId); return true; // 인증 통과! 컨트롤러로 진행 @@ -77,4 +77,4 @@ public class ApiKeyInterceptor implements HandlerInterceptor { // 6. 메모리 누수를 방지하기 위해 요청 처리가 완전히 끝나면 MDC에서 테넌트 정보를 지워줍니다. MDC.remove("tenantId"); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java index 3926a879..ea7da831 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/presentation/BusinessToolController.java @@ -34,14 +34,15 @@ public class BusinessToolController { @PostMapping("/mcp/{name}") public ResponseEntity executeDynamicTool( @PathVariable("name") String functionName, - @RequestHeader(value = "X-Request-Id", required = false) String headerRequestId, - @RequestHeader(value = "trace-id", required = false) String traceId, - @RequestHeader(value = "request-id", required = false) String requestId, - @RequestHeader(value = "employee-id", required = false) String encryptedEmployeeId, + @RequestHeader(value = "x-request-id", required = false) String requestId, + @RequestHeader(value = "guid", required = false) String guid, + @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 arguments) { ToolExecutionResult result = toolExecutionService.execute( functionName, - new McpRequestHeaders(headerRequestId, traceId, requestId, encryptedEmployeeId), + new McpRequestHeaders(requestId, guid, mcpSessionId, employeeNo, virtualEmployeeNo), arguments); ResponseEntity.BodyBuilder response = ResponseEntity.status(result.statusCode()); result.headers().forEach(response::header); diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java index 074a0b29..476d172d 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java @@ -9,8 +9,13 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat import com.fasterxml.jackson.databind.ObjectMapper; 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 java.lang.reflect.Method; +import java.util.Arrays; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.http.HttpMethod; import org.springframework.test.web.client.MockRestServiceServer; @@ -62,6 +67,68 @@ class AxhubHttpComponentTest { assertThat(response.status()).isEqualTo("OK"); 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 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) { } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java index 7b3fd577..d4e115a6 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/ToolExecutionServiceTest.java @@ -8,6 +8,7 @@ import io.shinhanlife.dap.lib.config.McpProperties; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import java.util.Map; +import java.util.Arrays; import org.junit.jupiter.api.Test; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; @@ -27,11 +28,35 @@ class McpToolExecutionServiceTest { void convertsArgumentsToDtoAndExecutesTheMatchedTool() { McpToolExecutionService service = serviceWith(new EchoTool()); 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("hello", ((Map) result.body()).get("value")); - assertEquals("trace-1", result.headers().get("trace-id")); - assertEquals("request-1", result.headers().get("request-id")); + assertEquals("request-1", result.headers().get("x-request-id")); + assertEquals("guid-1", result.headers().get("guid")); + assertEquals("session-1", result.headers().get("mcp-session-id")); + } + + private McpRequestHeaders headers(Map 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) { diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index db22407f..2373fbdb 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -31,13 +31,13 @@ class ToolScaffolderTest { new ToolScaffolder.ToolMethodDefinition( "CustomerGuidance", "searchGuidance", "CTMNILO00007", "Customer guidance", "Search guidance", "cmm", "MCI", false, "NILD", null, - List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)), - List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", "OPEN", false)), null), + List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", List.of("C001"), "", true)), + List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", List.of("OPEN"), "", false)), null), new ToolScaffolder.ToolMethodDefinition( "CustomerContract", "searchContract", "CTMCNT00001", "Customer contract", "Search contract", "cmm", "MCI", false, "CNTD", null, - List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)), - List.of(new ToolScaffolder.FieldDefinition("contractStatus", "String", "Contract status", "ACTIVE", false)), null))); + List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", List.of("C001"), "", true)), + 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"); String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java")); @@ -59,8 +59,8 @@ class ToolScaffolderTest { new ToolScaffolder.ToolMethodDefinition( "EmployeeSearch", "searchEmployee", null, "Employee search", "Search employee", "smp", "HTTP", false, null, "employee-search", - List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "Employee number", "10001", true)), - List.of(new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong", false)), null))); + List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "Employee number", List.of("10001"), "", true)), + 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"); assertTrue(Files.exists(glowConfig)); @@ -71,13 +71,13 @@ class ToolScaffolderTest { void generatesEnumAndListFieldsInDtoSchemaAndMockResponse() throws Exception { String moduleName = root.resolve("dap-was-claim").toString(); List 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()), - 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 outputFields = List.of( - new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", "", false, - List.of(), "Object", List.of(new ToolScaffolder.FieldDefinition("status", "String", "Status", "OPEN", true)))); + new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", List.of(), "", false, + 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, "tester", "2026.08.12", false, "CLM1", null, null, inputFields, outputFields); @@ -107,8 +107,8 @@ class ToolScaffolderTest { "분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.", "cmm", "MCI", moduleName, "테스터", "2026.08.12", false, null, null, null, - List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", "계약 분석", true)), - List.of(new ToolScaffolder.FieldDefinition("analysisResult", "String", "분석 결과", "정상", false))); + List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", List.of("계약 분석"), "", true)), + 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 useCase = sourceRoot.resolve("biz/cmm/usecase/AnalysisDataQueryUseCase.java"); @@ -146,7 +146,7 @@ class ToolScaffolderTest { ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 조회", "사번으로 재직 중인 직원을 조회한다.", "smp", "HTTP", moduleName, "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"); 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", "직원 조회", "직원을 조회한다.", "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); 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 { String moduleName = root.resolve("dap-was-pay").toString(); List inputFields = List.of( - new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true), - new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", "1", false)); + new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", List.of("EMP10001"), "", true), + new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", List.of("1"), "", false)); List 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, "tester", "2026.08.09", true, "DFAG", null, null, inputFields, outputFields); @@ -291,7 +291,7 @@ class ToolScaffolderTest { void generatesMockResponseAndUnitTestSkeletonFromOutputFields() throws Exception { String moduleName = root.resolve("dap-was-cus").toString(); List 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, "tester", "2026.08.10", true, null, null, null, List.of(), outputFields); @@ -318,12 +318,12 @@ class ToolScaffolderTest { void generatesDtoPackageAndRemovesDuplicateResponseFields() throws Exception { String moduleName = root.resolve("dap-was-http").toString(); List outputFields = List.of( - new ToolScaffolder.FieldDefinition("resultCode", "String", "API result", "SUCCESS", true), - new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong Gildong", false), - new ToolScaffolder.FieldDefinition("employeeName", "String", "Duplicate name", "Duplicate", false)); + new ToolScaffolder.FieldDefinition("resultCode", "String", "API result", List.of("SUCCESS"), "", true), + new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", List.of("Hong Gildong"), "", false), + new ToolScaffolder.FieldDefinition("employeeName", "String", "Duplicate name", List.of("Duplicate"), "", false)); List 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, "tester", "2026.08.11", false, null, null, null, inputFields, outputFields); @@ -434,10 +434,10 @@ class ToolScaffolderTest { void generatesObjectListAsNestedInnerClassWithoutSeparateItemSource() throws Exception { String moduleName = root.resolve("dap-was-inner-list").toString(); List 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( - new ToolScaffolder.FieldDefinition("date", "String", "date", "2026-08-13", true), - new ToolScaffolder.FieldDefinition("users", "Integer", "users", "10", false)))); + new ToolScaffolder.FieldDefinition("date", "String", "date", List.of("2026-08-13"), "", true), + new ToolScaffolder.FieldDefinition("users", "Integer", "users", List.of("10"), "", false)))); ToolScaffolder.scaffold("ga activity status", "GA001", "GA status", "GA status", "ana", "HTTP", moduleName, "tester", "2026.08.13", false, null, null, null, List.of(), fields); diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java deleted file mode 100644 index cd3be693..00000000 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/manifest/ToolManifestServiceTest.java +++ /dev/null @@ -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(); - } -} diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java index 159c27ff..e37c3ecc 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/mcp/McpRequestHeaderFilterTest.java @@ -1,17 +1,13 @@ 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.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.modelcontextprotocol.server.McpSyncServer; -import io.shinhanlife.dap.lib.mcp.*; -import java.lang.reflect.Method; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaderFilter; +import io.shinhanlife.dap.lib.mcp.McpRequestHeaders; +import java.lang.reflect.RecordComponent; +import java.util.LinkedHashMap; import java.util.Map; import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -20,41 +16,34 @@ import org.springframework.mock.web.MockHttpServletResponse; class McpRequestHeaderFilterTest { @Test - void capturesOptionalMcpHeadersOnlyForTheCurrentRequest() throws Exception { + void capturesDapmsHeadersOnlyForTheCurrentRequest() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp"); - request.addHeader("X-Request-Id", "gateway-request-id"); - request.addHeader("trace-id", "trace-001"); - request.addHeader("request-id", "tool-request-001"); - request.addHeader("employee-id", "encrypted-employee-id"); + request.addHeader("x-request-id", "request-001"); + request.addHeader("guid", "guid-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) -> { - McpRequestHeaders headers = McpRequestHeaderContext.current(); - assertEquals("gateway-request-id", headers.headerRequestId()); - assertEquals("trace-001", headers.traceId()); - assertEquals("tool-request-001", headers.requestId()); - assertEquals("encrypted-employee-id", headers.encryptedEmployeeId()); - }); + 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)"))); assertNull(McpRequestHeaderContext.current()); } - @Test - void forwardsCapturedHeadersToSharedToolExecutionService() throws Exception { - McpToolExecutionService service = mock(McpToolExecutionService.class); - McpRequestHeaders headers = new McpRequestHeaders( - "gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"); - doReturn(new ToolExecutionResult(200, Map.of("result", "ok"), Map.of())) - .when(service).execute(eq("sampleTool"), eq(headers), eq(Map.of("key", "value"))); - ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer( - mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), service, new ObjectMapper()); - - 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")); + private Map asMap(McpRequestHeaders headers) { + try { + Map values = new LinkedHashMap<>(); + for (RecordComponent component : McpRequestHeaders.class.getRecordComponents()) { + values.put(component.getName(), component.getAccessor().invoke(headers)); + } + return values; + } catch (ReflectiveOperationException exception) { + throw new AssertionError(exception); + } } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java index 165b6aeb..84c3e977 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/BusinessToolControllerHeaderContractTest.java @@ -1,32 +1,30 @@ package io.shinhanlife.dap.mcc.presentation; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.Map; - +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.web.bind.annotation.RequestHeader; class BusinessToolControllerHeaderContractTest { @Test - void encryptedEmployeeIdHeaderIsOptional() throws Exception { - Method method = BusinessToolController.class.getDeclaredMethod( - "executeDynamicTool", - String.class, - String.class, - String.class, - String.class, - String.class, - Map.class); + void receivesTheHeadersForwardedByDapms() { + Method method = Arrays.stream(BusinessToolController.class.getDeclaredMethods()) + .filter(candidate -> candidate.getName().equals("executeDynamicTool")) + .findFirst() + .orElseThrow(); - Parameter employeeIdParameter = method.getParameters()[4]; - RequestHeader requestHeader = employeeIdParameter.getAnnotation(RequestHeader.class); + List headerNames = Arrays.stream(method.getParameters()) + .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()); - assertFalse(requestHeader.required()); + assertThat(headerNames).containsExactly( + "x-request-id", "guid", "mcp-session-id", "employee-no", "virtual-employee-no"); } -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index f24419ef..c2ccc555 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -115,7 +115,7 @@ services: context: . dockerfile: dap-was-pro/Dockerfile ports: - - "8085:8085" + - "8285:8085" environment: - TZ=Asia/Seoul - AXHUB_GATEWAY_URL=http://gateway:8081 @@ -132,7 +132,7 @@ services: context: . dockerfile: dap-was-sys/Dockerfile ports: - - "8086:8086" + - "8286:8086" environment: - TZ=Asia/Seoul - AXHUB_GATEWAY_URL=http://gateway:8081