From b8fd9492c68139e7eebf0744112fc481cbda106c Mon Sep 17 00:00:00 2001 From: jade Date: Tue, 18 Aug 2026 11:05:47 +0900 Subject: [PATCH] Refactor: Switch schema validator to MCP SDK DefaultJsonSchemaValidator (JSON Schema 2020-12) to match dapms --- .../lib/config/ToolSchemaConfiguration.java | 12 ++++++-- .../dap/lib/mcp/McpToolExecutionService.java | 9 +++--- .../dap/lib/util/PodScaffolder.java | 2 ++ .../dap/lib/util/ToolScaffolder.java | 30 +++++++++---------- .../ToolArgumentSchemaValidator.java | 23 +++++++------- .../dap/lib/util/JsonSchemaGeneratorTest.java | 22 +++++++------- .../ToolArgumentSchemaValidatorTest.java | 10 ++++--- 7 files changed, 57 insertions(+), 51 deletions(-) diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java index d0f8e2ac..d6a118c3 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/ToolSchemaConfiguration.java @@ -1,6 +1,8 @@ package io.shinhanlife.dap.lib.config; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; +import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import org.springframework.context.annotation.Bean; @@ -18,7 +20,13 @@ public class ToolSchemaConfiguration { } @Bean - public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper) { - return new ToolArgumentSchemaValidator(objectMapper); + public JsonSchemaValidator mcpJsonSchemaValidator() { + return new DefaultJsonSchemaValidator(); + } + + @Bean + public ToolArgumentSchemaValidator toolArgumentSchemaValidator(ObjectMapper objectMapper, + JsonSchemaValidator jsonSchemaValidator) { + return new ToolArgumentSchemaValidator(objectMapper, jsonSchemaValidator); } } 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 2a59d397..478ef271 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 @@ -1,12 +1,11 @@ package io.shinhanlife.dap.lib.mcp; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.Error; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse; import io.shinhanlife.dap.lib.util.ToolSchemaResolver; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import java.lang.reflect.Method; import java.util.HashMap; -import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -61,8 +60,8 @@ public class McpToolExecutionService { if (tool.method().getParameterCount() == 0 || Map.class.isAssignableFrom(tool.method().getParameterTypes()[0])) return null; try { Map schema = toolSchemaResolver.resolve(tool.annotation(), tool.hint(), tool.method().getParameterTypes()[0]); - List errors = toolArgumentSchemaValidator.validate(schema, arguments); - return errors.isEmpty() ? null : error(422, "INVALID_PARAM", "Tool arguments do not match the input schema", requestId); + 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); return null; @@ -82,7 +81,7 @@ public class McpToolExecutionService { Object methodResult, String requestId) { try { Map outputSchema = toolSchemaResolver.resolveOutput(tool.annotation(), tool.method().getReturnType(), tool.hint()); - if (!outputSchema.isEmpty() && !toolArgumentSchemaValidator.validateValue(outputSchema, methodResult).isEmpty()) { + 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) { diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java index 7974277d..48766b0c 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/PodScaffolder.java @@ -143,6 +143,7 @@ public class PodScaffolder { bundle-id: %s name-prefix: "" security: + api-key: ${TOOL_SERVER_API_KEY:tool-server-key} tenant-domains: TESTER-DEV: ALL """.formatted(portStr, moduleName, moduleName.replace("dap-", "")); @@ -339,6 +340,7 @@ public class PodScaffolder { - "%s:%s" environment: - TZ=Asia/Seoul + - TOOL_SERVER_API_KEY=${TOOL_SERVER_API_KEY:-tool-server-key} - AXHUB_GATEWAY_URL=http://gateway:8081 - AXHUB_TOOL_URL=http://%s:%s - GLOW_COMMUNICATION_MCI_HOST=http://mci-mock diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java index a8e97b41..5fdfbf4b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -16,12 +16,12 @@ import java.util.Scanner; /** * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다. - * + * * [실행 방법] * 방법 1. IDE(IntelliJ 등)에서 직접 실행 * - ToolScaffolder.java의 main 메서드를 실행합니다. * - 콘솔 질문에 차례대로 값을 입력하면 파일이 생성됩니다. - * + * * 방법 2. 명령줄에서 실행 * - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java * - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]" @@ -37,7 +37,7 @@ import java.util.Scanner; * 수정일 수정자 수정내용 * ---------- -------- --------------------------- * 2026.09.01 0986406 최초생성 - * + * * */ public class ToolScaffolder { @@ -515,7 +515,7 @@ public class ToolScaffolder { httpApiName = httpApiName == null || httpApiName.isBlank() ? toKebabCase(baseName) : httpApiName.trim(); String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - + Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); Path usecaseImplDir = usecaseDir.resolve("impl"); Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto")); @@ -700,7 +700,7 @@ public class ToolScaffolder { toolName, title, description, toolHintLine, baseName, baseName - ); + ); writeUtf8(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent); @@ -798,7 +798,7 @@ public class ToolScaffolder { baseName, baseName, baseName - ); + ); String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", "."); serviceImplContent = serviceImplContent .replace("import " + mciIoPackage + ".io." + interfaceId + "_I;", @@ -874,9 +874,9 @@ public class ToolScaffolder { baseName, baseName, routingType, interfaceId - ); + ); } - + writeUtf8(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent); if (isMci) { @@ -992,10 +992,10 @@ public class ToolScaffolder { baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId - ); + ); converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId); writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent); - + log.append("\n=========================================\n"); log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); log.append("=========================================\n"); @@ -1040,9 +1040,9 @@ public class ToolScaffolder { } } """.formatted( - BASE_PACKAGE, mciGroupPath.replace("/", "."), + BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap - ); + ); writeUtf8(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent); log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n"); } @@ -1184,7 +1184,7 @@ public class ToolScaffolder { bizPackage, baseName, bizPackage, baseName, author, createDate, createDate, author, baseName, baseName, baseName, baseName, baseName, baseName, baseName - ); + ); converterContent = legacyConverterContent(bizPackage, baseName); writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent); @@ -1669,7 +1669,7 @@ public class ToolScaffolder { } private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage, - String httpApiClass, String author, String createDate) { + String httpApiClass, String author, String createDate) { String httpRequestClass = baseName + "HttpRequest"; String httpResponseClass = baseName + "HttpResponse"; String httpClientClass = httpApiClass + "Client"; @@ -1951,7 +1951,7 @@ public class ToolScaffolder { FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.examples(), field.pattern(), false); return "[" + mockValue(item) + "]"; } - + String exampleStr = (field.examples() != null && !field.examples().isEmpty()) ? field.examples().get(0) : null; if (exampleStr == null || exampleStr.isBlank()) { return "null"; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java index 46e54ea6..19bbc4bb 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java @@ -1,30 +1,27 @@ package io.shinhanlife.dap.lib.validation; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.Error; -import com.networknt.schema.InputFormat; -import com.networknt.schema.Schema; -import com.networknt.schema.SchemaRegistry; -import com.networknt.schema.SpecificationVersion; -import java.util.List; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse; import java.util.Map; -/** Validates tool arguments with the NetworkNT version selected by the MCP SDK. */ +/** Validates tool arguments with the MCP SDK JsonSchemaValidator (JSON Schema 2020-12). */ public class ToolArgumentSchemaValidator { private final ObjectMapper objectMapper; + private final JsonSchemaValidator jsonSchemaValidator; - public ToolArgumentSchemaValidator(ObjectMapper objectMapper) { + public ToolArgumentSchemaValidator(ObjectMapper objectMapper, JsonSchemaValidator jsonSchemaValidator) { this.objectMapper = objectMapper; + this.jsonSchemaValidator = jsonSchemaValidator; } - public List validate(Map schemaDefinition, Map arguments) throws Exception { + public ValidationResponse validate(Map schemaDefinition, Map arguments) { return validateValue(schemaDefinition, arguments); } - public List validateValue(Map schemaDefinition, Object value) throws Exception { - SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); - Schema schema = schemaRegistry.getSchema(objectMapper.writeValueAsString(schemaDefinition)); - return schema.validate(objectMapper.writeValueAsString(value), InputFormat.JSON); + public ValidationResponse validateValue(Map schemaDefinition, Object value) { + Object converted = objectMapper.convertValue(value, Object.class); + return jsonSchemaValidator.validate(schemaDefinition, converted); } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java index 2381031a..3af6be26 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/JsonSchemaGeneratorTest.java @@ -7,11 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.Error; -import com.networknt.schema.InputFormat; -import com.networknt.schema.Schema; -import com.networknt.schema.SchemaRegistry; -import com.networknt.schema.SpecificationVersion; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; +import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -47,7 +44,7 @@ class JsonSchemaGeneratorTest { assertEquals("object", childSchema.get("type")); assertTrue(required(childSchema).contains("businessDate")); - assertEquals("^\\d{8}$", property(childSchema, "businessDate").get("pattern")); + assertEquals("^\\\\d{8}$", property(childSchema, "businessDate").get("pattern")); } @Test @@ -62,12 +59,13 @@ class JsonSchemaGeneratorTest { @Test void validatorRejectsInvalidNestedValue() throws Exception { ObjectMapper objectMapper = new ObjectMapper(); - Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7) - .getSchema(objectMapper.writeValueAsString(JsonSchemaGenerator.generateSchema(NestedRequest.class))); - List errors = schema.validate(objectMapper.writeValueAsString(Map.of( - "child", Map.of("businessDate", "2026-07-28"))), InputFormat.JSON); + JsonSchemaValidator validator = new DefaultJsonSchemaValidator(); + Map schema = JsonSchemaGenerator.generateSchema(NestedRequest.class); + Object arguments = objectMapper.convertValue( + Map.of("child", Map.of("businessDate", "2026-07-28")), Object.class); + JsonSchemaValidator.ValidationResponse result = validator.validate(schema, arguments); - assertFalse(errors.isEmpty()); + assertFalse(result.valid()); } @SuppressWarnings("unchecked") @@ -124,7 +122,7 @@ class JsonSchemaGeneratorTest { private static class NestedChild { @io.swagger.v3.oas.annotations.media.Schema( requiredMode = io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED, - pattern = "^\\d{8}$") + pattern = "^\\\\d{8}$") private String businessDate; } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java index c62bf519..d56bb5e7 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/mcc/presentation/ToolArgumentSchemaValidatorTest.java @@ -4,6 +4,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import java.util.List; import java.util.Map; @@ -11,16 +12,17 @@ import org.junit.jupiter.api.Test; class ToolArgumentSchemaValidatorTest { - private final ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper()); + private final ToolArgumentSchemaValidator validator = + new ToolArgumentSchemaValidator(new ObjectMapper(), new DefaultJsonSchemaValidator()); @Test - void validatesDraft7SchemaWithTheRuntimeNetworkntVersion() throws Exception { + void validatesJsonSchema202012WithTheMcpSdkValidator() { Map schema = Map.of( "type", "object", "properties", Map.of("name", Map.of("type", "string")), "required", List.of("name")); - assertTrue(validator.validate(schema, Map.of("name", "Hong")).isEmpty()); - assertFalse(validator.validate(schema, Map.of()).isEmpty()); + assertTrue(validator.validate(schema, Map.of("name", "Hong")).valid()); + assertFalse(validator.validate(schema, Map.of()).valid()); } }