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 d0f8e2ac9..d6a118c33 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 2a59d3976..478ef271e 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/validation/ToolArgumentSchemaValidator.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/validation/ToolArgumentSchemaValidator.java index 46e54ea64..19bbc4bbd 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/config/ToolSchemaConfigurationTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java index c33789f25..352237b48 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/config/ToolSchemaConfigurationTest.java @@ -3,6 +3,7 @@ package io.shinhanlife.dap.lib.config; import static org.junit.jupiter.api.Assertions.assertNotNull; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.schema.JsonSchemaValidator; import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -17,6 +18,7 @@ class ToolSchemaConfigurationTest { context.refresh(); assertNotNull(context.getBean(ToolArgumentSchemaValidator.class)); + assertNotNull(context.getBean(JsonSchemaValidator.class)); } } } 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 d4e115a62..e955f401a 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 @@ -3,12 +3,13 @@ package io.shinhanlife.dap.lib.mcp; import static org.junit.jupiter.api.Assertions.assertEquals; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator; import io.shinhanlife.dap.lib.annotation.GrowToolHint; 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 java.util.Map; import org.junit.jupiter.api.Test; import org.springaicommunity.mcp.annotation.McpTool; import org.springaicommunity.mcp.annotation.McpToolParam; @@ -67,7 +68,8 @@ class McpToolExecutionServiceTest { McpToolMethodRegistry registry = new McpToolMethodRegistry(context, new McpProperties()); registry.initialize(); return new McpToolExecutionService(registry, objectMapper, - new ToolArgumentSchemaValidator(objectMapper), new ToolSchemaResolver(objectMapper)); + new ToolArgumentSchemaValidator(objectMapper, new DefaultJsonSchemaValidator()), + new ToolSchemaResolver(objectMapper)); } static class EchoTool { 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 2381031a2..3af6be26e 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 c62bf5190..d56bb5e7c 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()); } }